From b5c85913f6020c026faaea3d5d00f7a74e774d07 Mon Sep 17 00:00:00 2001 From: TiaTuinstra Date: Fri, 21 Aug 2026 13:42:20 +0000 Subject: [PATCH 1/6] feat: Switch from fuzzy matching and data normalization of input file to enforced frictionless schema --- README.md | 1 + config/input_schema.yaml | 37 + docs/user_guide/getting_started.md | 32 + input/rodent_dataset.xlsx | Bin 10465 -> 10543 bytes pipeline/generate_notices.py | 7 +- pipeline/orchestrator.py | 5 +- pipeline/preprocess.py | 287 +++---- pyproject.toml | 2 +- tests/fixtures/sample_input.py | 74 +- tests/integration/test_pipeline_contracts.py | 32 +- tests/unit/test_generate_notices.py | 15 + tests/unit/test_orchestrator.py | 5 +- tests/unit/test_preprocess.py | 439 ++++------ uv.lock | 836 +++++++++++++++++-- 14 files changed, 1150 insertions(+), 622 deletions(-) create mode 100644 config/input_schema.yaml diff --git a/README.md b/README.md index 823f1a0..53f899e 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,7 @@ uv run pytest -m "not e2e" - Use data extracts from [Panorama PEAR](https://accessonehealth.ca/) - Place input files in the `input/` subfolder (not tracked by Git) - Files must be `.xlsx` format with a **single worksheet** per file + - Column names must match the required schema exactly - see [Getting Started](docs/user_guide/getting_started.md#preparing-input-data) for the full column list ## Preprocessing diff --git a/config/input_schema.yaml b/config/input_schema.yaml new file mode 100644 index 0000000..76a18c3 --- /dev/null +++ b/config/input_schema.yaml @@ -0,0 +1,37 @@ +fields: + - name: School Type + type: string + - name: School Name + type: string + - name: Client Id + type: string + constraints: + pattern: '^\d{10}$' + - name: First Name + type: string + - name: Last Name + type: string + - name: Age + type: integer + - name: Date of Birth + type: date + - name: Street Address Line 1 + type: string + - name: Street Address Line 2 + type: string + - name: City + type: string + - name: Province/Territory + type: string + - name: Postal Code + type: string + - name: Overdue Disease + type: string + - name: Overdue Agent + type: string + - name: Imms Given + type: string + - name: Birth Year + type: string +missingValues: + - '' \ No newline at end of file diff --git a/docs/user_guide/getting_started.md b/docs/user_guide/getting_started.md index 67603c7..5b2380b 100644 --- a/docs/user_guide/getting_started.md +++ b/docs/user_guide/getting_started.md @@ -27,6 +27,38 @@ uv run pre-commit install Input files must be `.xlsx` format with a single worksheet, extracted from [Panorama PEAR](https://accessonehealth.ca/). +The pipeline enforces a strict column schema — column names must match exactly (no fuzzy matching). The following columns are **required**: + +| Column name | Notes | +|---|---| +| `School Type` | | +| `School Name` | | +| `Client Id` | 10-digit numeric string | +| `First Name` | | +| `Last Name` | | +| `Age` | Integer | +| `Date of Birth` | ISO 8601 date (`YYYY-MM-DD`) | +| `Street Address Line 1` | | +| `Street Address Line 2` | May be blank | +| `City` | | +| `Province/Territory` | | +| `Postal Code` | | +| `Overdue Disease` | May be blank | +| `Overdue Agent` | May be blank | +| `Imms Given` | May be blank | +| `Birth Year` | | + +The following columns are **optional** and will be used when present: + +| Column name | +|---| +| `Board Name` | +| `Board Id` | +| `School Id` | +| `Unique Id` | + +The full schema is defined in [`config/input_schema.yaml`](../../config/input_schema.yaml). If the file is missing any required column, the pipeline will stop immediately with a clear error message listing the missing columns. + Place input files in the `input/` subdirectory (not tracked by Git): ``` diff --git a/input/rodent_dataset.xlsx b/input/rodent_dataset.xlsx index c8991c38b1b4f734d2ecf822c3aabfc5d1cfa109..e8effd78f119c6427131dcb6fd8496d744af8cc8 100644 GIT binary patch delta 4994 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 delta 4869 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 diff --git a/pipeline/generate_notices.py b/pipeline/generate_notices.py index 76a7bfc..e0fb892 100644 --- a/pipeline/generate_notices.py +++ b/pipeline/generate_notices.py @@ -293,6 +293,9 @@ def read_artifact(path: Path) -> ArtifactPayload: ) +_TYP_IDENT_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_-]*$") + + def escape_string(value: str) -> str: """Escape special characters in a string for Typst template output. @@ -359,7 +362,9 @@ def to_typ_value(value) -> str: inner = ", ".join(items) return f"({inner})" if isinstance(value, Mapping): - items = ", ".join(f"{key}: {to_typ_value(val)}" for key, val in value.items()) + def _typ_key(k: str) -> str: + return k if _TYP_IDENT_RE.match(k) else f'"{escape_string(k)}"' + items = ", ".join(f"{_typ_key(key)}: {to_typ_value(val)}" for key, val in value.items()) return f"({items})" raise TypeError(f"Unsupported value type for Typst conversion: {type(value)!r}") diff --git a/pipeline/orchestrator.py b/pipeline/orchestrator.py index fd31df6..3ce5673 100755 --- a/pipeline/orchestrator.py +++ b/pipeline/orchestrator.py @@ -225,9 +225,8 @@ def run_step_2_preprocess( # Load and process input data input_path = input_dir / input_file df_raw = preprocess.read_input(input_path) - mapped_df, column_mapping = preprocess.map_columns(df_raw) - df_filtered = preprocess.filter_columns(mapped_df) - df = preprocess.normalize_dataframe(df_filtered) + preprocess.validate_input(input_path) + df = preprocess.normalize_dataframe(preprocess.map_columns(df_raw)) # Check that addresses are complete, return only complete rows df = preprocess.check_addresses_complete(df) diff --git a/pipeline/preprocess.py b/pipeline/preprocess.py index 1c48ec9..ea6f0ab 100644 --- a/pipeline/preprocess.py +++ b/pipeline/preprocess.py @@ -52,11 +52,11 @@ from hashlib import sha1 from pathlib import Path from string import Formatter -from typing import Any, Dict, List, Literal, Optional, overload +from typing import Any, Dict, List, Literal, Optional import pandas as pd import yaml from babel.dates import format_date -from rapidfuzz import fuzz, process +from frictionless import Detector, Schema, validate as fl_validate from .data_models import ( ArtifactPayload, @@ -83,22 +83,33 @@ "Not Specified-unspecified", ] -REQUIRED_COLUMNS = [ - "SCHOOL NAME", - "CLIENT ID", - "FIRST NAME", - "LAST NAME", - "DATE OF BIRTH", - "CITY", - "POSTAL CODE", - "PROVINCE/TERRITORY", - "OVERDUE DISEASE", - "IMMS GIVEN", - "STREET ADDRESS LINE 1", - "STREET ADDRESS LINE 2", -] - -THRESHOLD = 80 +INPUT_SCHEMA_PATH = CONFIG_DIR / "input_schema.yaml" + +REQUIRED_COLUMN_MAP: dict[str, str] = { + "School Type": "SCHOOL_TYPE", + "School Name": "SCHOOL_NAME", + "Client Id": "CLIENT_ID", + "First Name": "FIRST_NAME", + "Last Name": "LAST_NAME", + "Age": "AGE", + "Date of Birth": "DATE_OF_BIRTH", + "Street Address Line 1": "STREET_ADDRESS_LINE_1", + "Street Address Line 2": "STREET_ADDRESS_LINE_2", + "City": "CITY", + "Province/Territory": "PROVINCE", + "Postal Code": "POSTAL_CODE", + "Overdue Disease": "OVERDUE_DISEASE", + "Overdue Agent": "OVERDUE_AGENT", + "Imms Given": "IMMS_GIVEN", + "Birth Year": "BIRTH_YEAR", +} + +OPTIONAL_COLUMN_MAP: dict[str, str] = { + "Board Name": "BOARD_NAME", + "Board Id": "BOARD_ID", + "School Id": "SCHOOL_ID", + "Unique Id": "UNIQUE_ID", +} def convert_date_string( @@ -367,7 +378,7 @@ def read_input(file_path: Path) -> pd.DataFrame: try: if ext in [".xlsx", ".xls"]: - df = pd.read_excel(file_path, engine="openpyxl", dtype={"CLIENT ID": str}) + df = pd.read_excel(file_path, engine="openpyxl", dtype={"Client Id": str}) elif ext == ".csv": # Try common encodings for enc in ["utf-8-sig", "latin-1", "cp1252"]: @@ -392,16 +403,69 @@ def read_input(file_path: Path) -> pd.DataFrame: raise -def normalize(col: str) -> str: - """Normalize formatting prior to matching.""" +def validate_input(file_path: Path) -> None: + """Validate that the input file conforms to the expected column schema. - # Trim whitespaces - col_normalized = col.lower().strip().replace("_", " ").replace("-", " ") + Parameters + ---------- + file_path : Path + Path to the input file (.xlsx or .csv). - # Check to see if double whitespace - col_normalized = re.sub(r"\s+", " ", col_normalized) + Raises + ------ + ValueError + If the file does not conform to the schema defined in + ``config/input_schema.yaml``. + """ + descriptor = yaml.safe_load(INPUT_SCHEMA_PATH.read_text(encoding="utf-8")) + schema = Schema.from_descriptor(descriptor) + report = fl_validate( + file_path.name, + basepath=str(file_path.parent), + schema=schema, + detector=Detector(schema_sync=True), + ) - return col_normalized + if not report.valid: + errors = report.flatten(["message"]) + raise ValueError( + "Input file does not conform to expected schema:\n" + + "\n".join(f" - {e[0]}" for e in errors) + ) + + +def map_columns(df: pd.DataFrame) -> pd.DataFrame: + """Rename input columns to internal UPPER_SNAKE_CASE keys. + + Required columns are validated for presence; optional columns are renamed + only when present. Columns outside both maps are dropped. + + Parameters + ---------- + df : pd.DataFrame + Raw input DataFrame with source column names as loaded from the input file. + + Returns + ------- + pd.DataFrame + DataFrame with columns renamed to the internal keys defined in + ``REQUIRED_COLUMN_MAP`` and ``OPTIONAL_COLUMN_MAP``. Unrecognised + columns are dropped. + + Raises + ------ + ValueError + If any required column is missing from the DataFrame. + """ + missing = [col for col in REQUIRED_COLUMN_MAP if col not in df.columns] + if missing: + raise ValueError(f"Input is missing required columns: {missing}") + + present_optional = {k: v for k, v in OPTIONAL_COLUMN_MAP.items() if k in df.columns} + full_map = {**REQUIRED_COLUMN_MAP, **present_optional} + renamed = df.rename(columns=full_map) + known = set(full_map.values()) + return renamed[[col for col in renamed.columns if col in known]] def split_vaccine_due_entry(item: str) -> tuple[str, str | None]: @@ -458,161 +522,28 @@ def format_vaccine_due_list(vaccine_due_list: list[str]) -> list[str]: return formatted -def map_columns(df: pd.DataFrame, required_columns=REQUIRED_COLUMNS): - """ - Map dataframe columns to a set of required column names using fuzzy matching. - Parameters - ---------- - df : pandas.DataFrame - Input dataframe whose columns will be matched and optionally renamed. - required_columns : Sequence[str], optional - Sequence of expected/required column names to match against. Defaults to REQUIRED_COLUMNS. - Returns - ------- - tuple[pandas.DataFrame, dict] - A tuple (renamed_df, col_map) where `renamed_df` is `df` with columns renamed according to successful matches, - and `col_map` is a dict mapping original column names (keys) to matched required column names (values). - Behavior - -------- - - Normalizes input column names and required column names using `normalize(...)` before matching. - - For each normalized input column, finds the best fuzzy match among normalized `required_columns` using - `process.extractOne(..., scorer=fuzz.partial_ratio)`. - - If the best match score is >= 80 (threshold in the implementation), the original input column name is mapped to the - corresponding required column name; the mapping is recorded and the dataframe is renamed accordingly. - - A debug line is printed for each accepted match: "Matching '' to '' with score ". - - Columns with a best match score < 80 are ignored (not included in `col_map`); matches with score 0 are effectively dropped. - - The code resolves the original column name by locating the first column whose normalized form equals the normalized input. - If multiple original columns normalize to the same value, the first encountered is used. - Notes - ----- - - The function depends on external helpers: `normalize`, `process.extractOne`, and `fuzz.partial_ratio`. - - The match threshold (80) is adjustable; lowering it makes matching more permissive, raising it makes it stricter. - - A `StopIteration` may occur if a normalized input column cannot be resolved back to an original column name. - - `required_columns` should be an iterable of strings; `df.columns` are expected to be convertible to strings. - Examples - -------- - # Example usage (illustrative only): - # renamed_df, mapping = map_columns(df, required_columns=['state', 'date', 'count']) - """ - input_cols = df.columns - col_map = {} - normalized_required = [normalize(req) for req in required_columns] - best_matches = {} - - # Check each input column against required columns - for actual_in_col in input_cols: - input_col = normalize(actual_in_col) - result = process.extractOne( - query=input_col, - choices=normalized_required, - scorer=fuzz.partial_ratio, - ) - if result is None: - continue - - _, score, index = result - if score < THRESHOLD: - continue - - best_match = required_columns[index] - print(f"Matching '{input_col}' to '{best_match}' with score {score}") - - prior = best_matches.get(best_match) - if prior is None: - print( - f"The value {best_match} does not exist in the dictionary - adding value." - ) - col_map[actual_in_col] = best_match - best_matches[best_match] = {"actual_in_col": actual_in_col, "score": score} - elif score > prior["score"]: - print( - f"{input_col} has a higher score than current best match in the dictionary - replacing value." - ) - col_map.pop(prior["actual_in_col"], None) - col_map[actual_in_col] = best_match - best_matches[best_match] = {"actual_in_col": actual_in_col, "score": score} - - return df.rename(columns=col_map), col_map - - -@overload -def filter_columns( - df: pd.DataFrame, required_columns: list[str] = REQUIRED_COLUMNS -) -> pd.DataFrame: ... - - -@overload -def filter_columns( - df: None, required_columns: list[str] = REQUIRED_COLUMNS -) -> None: ... - - -def filter_columns( - df: pd.DataFrame | None, required_columns: list[str] = REQUIRED_COLUMNS -) -> pd.DataFrame | None: - """Filter dataframe to only include required columns.""" - if df is None or df.empty: - return df - - return df[[col for col in df.columns if col in required_columns]] - - def normalize_dataframe(df: pd.DataFrame) -> pd.DataFrame: - """Validate, rename, and normalize the input DataFrame. - - Combines column validation with type normalization. Standardizes column - names to uppercase with underscores, validates all required columns are - present, then normalizes data types and fills missing values. + """Normalize data types on a column-mapped DataFrame. - Parameters - ---------- - df : pd.DataFrame - Input DataFrame with raw client data (column names may have mixed - case/spacing). - - Returns - ------- - pd.DataFrame - Copy of DataFrame with validated, renamed, and normalized columns. - - Raises - ------ - ValueError - If any required columns are missing from the DataFrame. + Expects columns already renamed to UPPER_SNAKE_CASE by map_columns(). + Applies string normalization, date parsing, and numeric coercion. """ working = df.copy() - # Normalize and validate column names in a single pass - def _normalize_col(col: str) -> str: - col = col.strip().upper().replace(" ", "_") - return "PROVINCE" if col == "PROVINCE/TERRITORY" else col - - working.columns = [_normalize_col(col) for col in working.columns] - - _required_normalized = {_normalize_col(col) for col in REQUIRED_COLUMNS} - missing = [col for col in _required_normalized if col not in working.columns] - if missing: - raise ValueError( - f"Missing required columns: {missing} \n Found columns: {list(working.columns)} " - ) + _skip = {"CLIENT_ID", "DATE_OF_BIRTH", "OVERDUE_DISEASE", "IMMS_GIVEN", "AGE"} + string_required = [v for v in REQUIRED_COLUMN_MAP.values() if v not in _skip] - # Normalize data types and fill missing values. - # Required string columns are derived from REQUIRED_COLUMNS; optional - # supplemental columns are listed separately. - _non_string_required = {"CLIENT_ID", "DATE_OF_BIRTH", "OVERDUE_DISEASE", "IMMS_GIVEN"} - required_string_cols = [col for col in _required_normalized if col not in _non_string_required] - optional_string_cols = ["SCHOOL_TYPE", "BOARD_NAME", "BOARD_ID", "SCHOOL_ID", "UNIQUE_ID"] + for col in string_required: + working[col] = working[col].fillna(" ").astype(str).str.strip() - for column in required_string_cols + optional_string_cols: - if column not in working.columns: - working[column] = "" - working[column] = working[column].fillna(" ").astype(str).str.strip() + for col in OPTIONAL_COLUMN_MAP.values(): + if col not in working.columns: + working[col] = "" + else: + working[col] = working[col].fillna(" ").astype(str).str.strip() working["DATE_OF_BIRTH"] = pd.to_datetime(working["DATE_OF_BIRTH"], errors="coerce") - if "AGE" in working.columns: - working["AGE"] = pd.to_numeric(working["AGE"], errors="coerce") - else: - working["AGE"] = pd.NA + working["AGE"] = pd.to_numeric(working["AGE"], errors="coerce") return working @@ -1011,6 +942,7 @@ def build_received_rows( replace_unspecified: List[str], vaccine_reference: Dict[str, Any], chart_diseases_header: List[str], + show_validity_markers: bool = False, ) -> List[Dict[str, Any]]: """Parse IMMS_GIVEN into display rows with pre-computed per-column validity. @@ -1058,7 +990,11 @@ def build_received_rows( rows: List[Dict[str, Any]] = [] for date, doses in by_date.items(): vaccines = _deduplicate_vaccines_for_date(doses, vaccine_reference) - date_rows = _split_into_rows(vaccines, chart_diseases_header) + if show_validity_markers: + date_rows = _split_into_rows(vaccines, chart_diseases_header) + else: + columns = compute_column_statuses(vaccines, chart_diseases_header) + date_rows = [{"vaccines": vaccines, "columns": columns}] n = len(date_rows) for i, row in enumerate(date_rows): rows.append({ @@ -1090,8 +1026,8 @@ def build_preprocess_result( ---------- df : pd.DataFrame Raw input DataFrame, typically loaded from an Excel or CSV file. - Must contain all columns listed in ``REQUIRED_COLUMNS`` (column - names are fuzzy-matched, so spacing and case variants are accepted). + Must have columns already renamed via map_columns() to the internal + UPPER_SNAKE_CASE keys defined in ``REQUIRED_COLUMN_MAP``. language : str Language code for this batch (``"en"`` or ``"fr"``). Stored on every ``ClientRecord`` and used to format display dates. @@ -1234,6 +1170,7 @@ def build_preprocess_result( replace_unspecified, vaccine_reference, chart_diseases_header, + show_validity_markers, ) postal_code = row.POSTAL_CODE if row.POSTAL_CODE else "Not provided" # type: ignore[attr-defined] address_line = " ".join( diff --git a/pyproject.toml b/pyproject.toml index dee4f9f..8209e16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "qrcode>=7.4.2", "pillow>=12.3.0", "babel>=2.17.0", - "rapidfuzz" + "frictionless[excel,csv]" ] [dependency-groups] diff --git a/tests/fixtures/sample_input.py b/tests/fixtures/sample_input.py index 8b1ff97..712e163 100644 --- a/tests/fixtures/sample_input.py +++ b/tests/fixtures/sample_input.py @@ -49,65 +49,73 @@ def create_test_input_dataframe( DataFrame with columns matching expected Excel input format """ data: Dict[str, List[Any]] = { - "SCHOOL NAME": [ + "School Type": ["Public", "Public", "Catholic", "Catholic", "Public"][:num_clients], + "School Name": [ "Tunnel Academy", "Cheese Wheel Academy", "Mountain Heights Public School", "River Valley Elementary", "Downtown Collegiate", ][:num_clients], - "CLIENT ID": [f"{i:010d}" for i in range(1, num_clients + 1)], - "FIRST NAME": ["Alice", "Benoit", "Chloe", "Diana", "Ethan"][:num_clients], - "LAST NAME": ["Zephyr", "Arnaud", "Brown", "Davis", "Evans"][:num_clients], - "DATE OF BIRTH": [ + "Client Id": [f"{i:010d}" for i in range(1, num_clients + 1)], + "First Name": ["Alice", "Benoit", "Chloe", "Diana", "Ethan"][:num_clients], + "Last Name": ["Zephyr", "Arnaud", "Brown", "Davis", "Evans"][:num_clients], + "Age": ["10", "11", "12", "10", "11"][:num_clients], + "Date of Birth": [ "2015-01-02", "2014-05-06", "2013-08-15", "2015-03-22", "2014-11-10", ][:num_clients], - "SCHOOL BOARD NAME": [ + "Board Name": [ "Guelph Board of Education", "Guelph Board of Education", "Wellington Board of Education", "Wellington Board of Education", "Ontario Public Schools", ][:num_clients], - "CITY": ["Guelph", "Guelph", "Wellington", "Wellington", "Toronto"][ - :num_clients - ], - "POSTAL CODE": ["N1H 2T2", "N1H 2T3", "N1K 1B2", "N1K 1B3", "M5V 3A8"][ - :num_clients - ], - "PROVINCE/TERRITORY": ["ON", "ON", "ON", "ON", "ON"][:num_clients], - "STREET ADDRESS LINE 1": [ + "Street Address Line 1": [ "123 Main St", "456 Side Rd", "789 Oak Ave", "321 Elm St", "654 Maple Dr", ][:num_clients], - "STREET ADDRESS LINE 2": ["", "Suite 5", "", "Apt 12", ""][:num_clients], + "Street Address Line 2": ["", "Suite 5", "", "Apt 12", ""][:num_clients], + "City": ["Guelph", "Guelph", "Wellington", "Wellington", "Toronto"][:num_clients], + "Province/Territory": ["ON", "ON", "ON", "ON", "ON"][:num_clients], + "Postal Code": ["N1H 2T2", "N1H 2T3", "N1K 1B2", "N1K 1B3", "M5V 3A8"][:num_clients], + "Overdue Disease": ( + [ + "Measles/Mumps/Rubella", + "Haemophilus influenzae infection, invasive", + "Diphtheria/Tetanus/Pertussis", + "Polio", + "Pneumococcal infection, invasive", + ][:num_clients] + if include_overdue + else [""] * num_clients + ), + "Overdue Agent": ( + ["MMR", "Hib", "DTaP", "IPV", "PCV13"][:num_clients] + if include_overdue + else [""] * num_clients + ), + "Imms Given": ( + [ + "May 01, 2020 - DTaP; Jun 15, 2021 - MMR", + "Apr 10, 2019 - IPV", + "Sep 05, 2020 - Varicella", + "", + "Jan 20, 2022 - DTaP; Feb 28, 2022 - IPV", + ][:num_clients] + if include_immunization_history + else [""] * num_clients + ), + "Birth Year": ["2015", "2014", "2013", "2015", "2014"][:num_clients], } - if include_overdue: - data["OVERDUE DISEASE"] = [ - "Measles/Mumps/Rubella", - "Haemophilus influenzae infection, invasive", - "Diphtheria/Tetanus/Pertussis", - "Polio", - "Pneumococcal infection, invasive", - ][:num_clients] - - if include_immunization_history: - data["IMMS GIVEN"] = [ - "May 01, 2020 - DTaP; Jun 15, 2021 - MMR", - "Apr 10, 2019 - IPV", - "Sep 05, 2020 - Varicella", - "", - "Jan 20, 2022 - DTaP; Feb 28, 2022 - IPV", - ][:num_clients] - return pd.DataFrame(data) diff --git a/tests/integration/test_pipeline_contracts.py b/tests/integration/test_pipeline_contracts.py index 62e453a..f3bc7e3 100644 --- a/tests/integration/test_pipeline_contracts.py +++ b/tests/integration/test_pipeline_contracts.py @@ -252,8 +252,8 @@ def test_disease_alias_normalized_to_canonical_name( Assertion: vaccines_due_list contains "Polio", not the raw alias "Poliomyelitis" """ - df = sample_input.create_test_input_dataframe(num_clients=1) - df["OVERDUE DISEASE"] = ["Poliomyelitis"] + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df["OVERDUE_DISEASE"] = ["Poliomyelitis"] result = preprocess.build_preprocess_result( df, @@ -300,8 +300,8 @@ def test_unknown_validity_warns_when_markers_enabled( ) monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) - df = sample_input.create_test_input_dataframe(num_clients=1) - df["IMMS GIVEN"] = ["May 1, 2020 - DTaP"] + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df["IMMS_GIVEN"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( df, @@ -351,9 +351,9 @@ def test_mixed_validity_with_markers_enabled_raises_value_error( ) monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) - df = sample_input.create_test_input_dataframe(num_clients=2) + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) # First client has a suffixed dose; second has an un-suffixed dose → mixed - df["IMMS GIVEN"] = [ + df["IMMS_GIVEN"] = [ "May 1, 2020 - DTaP - Valid", "Jun 15, 2021 - MMR", ] @@ -397,8 +397,8 @@ def test_mixed_validity_with_markers_disabled_warns_and_succeeds( ) monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) - df = sample_input.create_test_input_dataframe(num_clients=2) - df["IMMS GIVEN"] = [ + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) + df["IMMS_GIVEN"] = [ "May 1, 2020 - DTaP - Valid", "Jun 15, 2021 - MMR", ] @@ -448,8 +448,8 @@ def test_include_dose_formats_vaccines_due_list( ) monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) - df = sample_input.create_test_input_dataframe(num_clients=1) - df["OVERDUE DISEASE"] = ["DTaP - 2"] + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df["OVERDUE_DISEASE"] = ["DTaP - 2"] result = preprocess.build_preprocess_result( df, @@ -479,8 +479,8 @@ def test_include_dose_requires_dose_bearing_schema( "preprocess:\n include_dose: true\n", encoding="utf-8", ) - df = sample_input.create_test_input_dataframe(num_clients=1) - df["OVERDUE DISEASE"] = ["Polio"] + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df["OVERDUE_DISEASE"] = ["Polio"] with pytest.raises(ValueError, match="include_dose requires overdue entries"): preprocess.build_preprocess_result( @@ -507,8 +507,8 @@ def test_blank_dose_warns_and_displays_only_disease( "preprocess:\n include_dose: true\n", encoding="utf-8", ) - df = sample_input.create_test_input_dataframe(num_clients=1) - df["OVERDUE DISEASE"] = ["Polio - "] + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df["OVERDUE_DISEASE"] = ["Polio - "] result = preprocess.build_preprocess_result( df, @@ -558,9 +558,7 @@ def normalized_test_df() -> pd.DataFrame: real pipeline — after column mapping and normalization, before artifact build. """ raw = sample_input.create_test_input_dataframe(num_clients=3) - mapped, _ = preprocess.map_columns(raw) - filtered = preprocess.filter_columns(mapped) - return preprocess.normalize_dataframe(filtered) + return preprocess.normalize_dataframe(preprocess.map_columns(raw)) @pytest.mark.integration diff --git a/tests/unit/test_generate_notices.py b/tests/unit/test_generate_notices.py index 7ecbf33..7c454a9 100644 --- a/tests/unit/test_generate_notices.py +++ b/tests/unit/test_generate_notices.py @@ -253,6 +253,21 @@ def test_to_typ_value_dict(self) -> None: assert "John Doe" in result assert "age" in result + def test_to_typ_value_dict_with_non_identifier_keys(self) -> None: + """Verify dict keys with spaces/special chars are quoted in Typst output. + + Real-world significance: + - Disease names used as column-dict keys can contain spaces (e.g. "Hepatitis B") + - Bare identifiers with spaces are invalid Typst syntax + - Simple identifier keys (e.g. qr_url) must stay unquoted for dot-notation access + """ + data = {"Hepatitis B": "valid", "qr_url": "https://example.com"} + result = generate_notices.to_typ_value(data) + + assert '"Hepatitis B":' in result + assert "qr_url:" in result + assert '"https://example.com"' in result + def test_to_typ_value_unsupported_type_raises_error(self) -> None: """Verify error for unsupported types. diff --git a/tests/unit/test_orchestrator.py b/tests/unit/test_orchestrator.py index 8bffe82..7d13a6e 100644 --- a/tests/unit/test_orchestrator.py +++ b/tests/unit/test_orchestrator.py @@ -228,12 +228,9 @@ def test_run_step_2_passes_selected_config_path(self, tmp_path: Path) -> None: "pipeline.orchestrator.preprocess.read_input", return_value=MagicMock(), ), + patch("pipeline.orchestrator.preprocess.validate_input"), patch( "pipeline.orchestrator.preprocess.map_columns", - return_value=(MagicMock(), {}), - ), - patch( - "pipeline.orchestrator.preprocess.filter_columns", return_value=MagicMock(), ), patch( diff --git a/tests/unit/test_preprocess.py b/tests/unit/test_preprocess.py index 0925e43..cc3fae4 100644 --- a/tests/unit/test_preprocess.py +++ b/tests/unit/test_preprocess.py @@ -29,88 +29,74 @@ from tests.fixtures import sample_input -@pytest.mark.unit -class TestMapColumns: - """Unit tests for map_columns() column mapping utility.""" - - def test_maps_exact_column_names(self): - """Verify that exact column names are mapped correctly.""" - df = pd.DataFrame( - { - "SCHOOL NAME": ["Test School"], - "CLIENT ID": ["C001"], - "FIRST NAME": ["Alice"], - "LAST NAME": ["Zephyr"], - "DATE OF BIRTH": ["2015-01-01"], - "CITY": ["Guelph"], - "POSTAL CODE": ["N1H 2T2"], - "PROVINCE/TERRITORY": ["ON"], - "OVERDUE DISEASE": ["Measles"], - "IMMS GIVEN": [""], - "STREET ADDRESS LINE 1": ["123 Main"], - "STREET ADDRESS LINE 2": [""], - } - ) - - mapped_df, col_map = preprocess.map_columns(df) - - assert set(mapped_df.columns) == set(preprocess.REQUIRED_COLUMNS) - for col in preprocess.REQUIRED_COLUMNS: - assert col in col_map.values() - - def test_maps_inexact_column_names(self): - """Verify that inexact column names are mapped correctly.""" - df = pd.DataFrame( - { - "school-name": ["Test School"], - "client id": ["C001"], - "First Name": ["Alice"], - "last_name": ["Zephyr"], - "date-of-birth": ["2015-01-01"], - "City": ["Guelph"], - "postal_code": ["N1H 2T2"], - "province territory": ["ON"], - "overdue disease": ["Measles"], - "imms given": [""], - "street address line 1": ["123 Main"], - "street address line 2": [""], - } - ) - - mapped_df, col_map = preprocess.map_columns(df) - - assert set(mapped_df.columns) == set(preprocess.REQUIRED_COLUMNS) - for col in preprocess.REQUIRED_COLUMNS: - assert col in col_map.values() +def _make_conforming_df(**overrides) -> pd.DataFrame: + """Build a minimal DataFrame with all required input columns.""" + row = { + "School Type": ["Public"], + "School Name": ["Test School"], + "Client Id": ["C001"], + "First Name": ["Alice"], + "Last Name": ["Zephyr"], + "Age": ["10"], + "Date of Birth": ["2015-01-01"], + "Street Address Line 1": ["123 Main St"], + "Street Address Line 2": [""], + "City": ["Guelph"], + "Province/Territory": ["ON"], + "Postal Code": ["N1H 2T2"], + "Overdue Disease": ["Measles"], + "Overdue Agent": ["MMR"], + "Imms Given": [""], + "Birth Year": ["2015"], + } + row.update(overrides) + return pd.DataFrame(row) @pytest.mark.unit -class TestNormalize: - """Unit tests for normalize() column name formatter.""" - - def test_lowercases_text(self): - """Verify that text is converted to lowercase.""" - assert preprocess.normalize("ColumnName") == "columnname" - - def test_replaces_spaces_with_underscores(self): - """Verify that internal spaces are replaced with underscores.""" - assert preprocess.normalize("Column_Name") == "column name" - - def test_replaces_hyphens_with_underscores(self): - """Verify that hyphens are replaced with underscores.""" - assert preprocess.normalize("Column-Name") == "column name" - - def test_combined_transformations(self): - """Verify that multiple transformations apply together.""" - assert preprocess.normalize(" Column - Name ") == "column name" - - def test_handles_empty_string(self): - """Verify that empty strings are handled safely.""" - assert preprocess.normalize("") == "" - - def test_handles_non_alphabetic_characters(self): - """Verify that non-letter characters are preserved.""" - assert preprocess.normalize("123 Name!") == "123 name!" +class TestMapColumns: + """Unit tests for map_columns() strict column mapping.""" + + def test_renames_required_columns_to_internal_keys(self): + df = _make_conforming_df() + result = preprocess.map_columns(df) + assert set(preprocess.REQUIRED_COLUMN_MAP.values()).issubset(set(result.columns)) + + def test_province_territory_maps_to_province(self): + df = _make_conforming_df() + result = preprocess.map_columns(df) + assert "PROVINCE" in result.columns + assert "Province/Territory" not in result.columns + + def test_drops_unknown_columns(self): + df = _make_conforming_df() + df["Extra Column"] = ["surprise"] + result = preprocess.map_columns(df) + assert "Extra Column" not in result.columns + assert "EXTRA_COLUMN" not in result.columns + + def test_includes_optional_column_when_present(self): + df = _make_conforming_df() + df["Board Name"] = ["District Board"] + result = preprocess.map_columns(df) + assert "BOARD_NAME" in result.columns + + def test_omits_optional_column_when_absent(self): + df = _make_conforming_df() + result = preprocess.map_columns(df) + assert "BOARD_NAME" not in result.columns + + def test_raises_on_missing_required_column(self): + df = _make_conforming_df() + df = df.drop(columns=["School Name"]) + with pytest.raises(ValueError, match="missing required columns"): + preprocess.map_columns(df) + + def test_raises_listing_all_missing_columns(self): + df = _make_conforming_df() + df = df.drop(columns=["School Name", "First Name"]) + with pytest.raises(ValueError, match="missing required columns"): + preprocess.map_columns(df) @pytest.mark.unit @@ -172,73 +158,6 @@ def test_hides_supplied_doses_and_preserves_ordinary_entries(self) -> None: assert result == ["Polio", "MMR"] -@pytest.mark.unit -class TestFilterColumns: - """Unit tests for filter_columns() column filtering utility.""" - - def test_returns_only_required_columns(self): - """Verify that only required columns are kept.""" - df = pd.DataFrame( - { - "child_first_name": ["A"], - "child_last_name": ["B"], - "extra_column": [123], - } - ) - required = ["child_first_name", "child_last_name"] - result = preprocess.filter_columns(df, required) - - assert list(result.columns) == required - assert "extra_column" not in result.columns - - def test_returns_empty_dataframe_when_no_required_columns_present(self): - """Verify behavior when none of the required columns are present.""" - df = pd.DataFrame({"foo": [1], "bar": [2]}) - required = ["child_first_name", "child_last_name"] - result = preprocess.filter_columns(df, required) - - # Should return an empty DataFrame with no columns - assert result.shape[1] == 0 - assert isinstance(result, pd.DataFrame) - - def test_handles_empty_dataframe(self): - """Verify that an empty DataFrame is returned unchanged.""" - df = pd.DataFrame(columns=pd.Index(["child_first_name", "child_last_name"])) - result = preprocess.filter_columns(df, ["child_first_name"]) - assert result.empty - - def test_handles_none_input(self): - """Verify that None input returns None safely.""" - result = preprocess.filter_columns(None, ["child_first_name"]) # type: ignore[arg-type] - assert result is None - - def test_order_of_columns_is_preserved(self): - """Verify that the order of columns in the required list is respected.""" - df = pd.DataFrame( - { - "child_last_name": ["Doe"], - "child_first_name": ["John"], - "dob": ["2000-01-01"], - } - ) - required = ["dob", "child_first_name"] - result = preprocess.filter_columns(df, required) - - assert ( - list(result.columns) == ["child_first_name", "dob"] - or list(result.columns) == required - ) - # Either column order can appear depending on implementation; both are acceptable - - def test_ignores_required_columns_not_in_df(self): - """Verify that missing required columns are ignored without error.""" - df = pd.DataFrame({"child_first_name": ["A"]}) - required = ["child_first_name", "missing_column"] - result = preprocess.filter_columns(df, required) - - assert "child_first_name" in result.columns - assert "missing_column" not in result.columns - @pytest.mark.unit class TestReadInput: @@ -258,10 +177,7 @@ def test_read_input_xlsx_file(self, tmp_test_dir: Path) -> None: df_read = preprocess.read_input(input_path) assert len(df_read) == 3 - assert ( - "SCHOOL NAME" in df_read.columns - or "SCHOOL_NAME" in str(df_read.columns).upper() - ) + assert "School Name" in df_read.columns def test_read_input_missing_file_raises_error(self, tmp_test_dir: Path) -> None: """Verify error when input file doesn't exist. @@ -294,107 +210,39 @@ class TestNormalizeDataFrame: """Unit tests for normalize_dataframe function.""" def test_normalize_dataframe_passes_valid_dataframe(self) -> None: - """Verify valid DataFrame passes validation. - - Real-world significance: - - Valid school district input should process without errors - """ - df = sample_input.create_test_input_dataframe(num_clients=3) + """Verify valid DataFrame passes normalization without errors.""" + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) result = preprocess.normalize_dataframe(df) assert result is not None assert len(result) == 3 - def test_normalize_dataframe_normalizes_column_whitespace(self) -> None: - """Verify column names are normalized (whitespace, case). - - Real-world significance: - - Input files may have inconsistent column naming - - Pipeline must handle variations in Excel headers - """ - df = pd.DataFrame( - { - " SCHOOL NAME ": ["Test School"], - " CLIENT ID ": ["C001"], - "first name": ["Alice"], - "last name": ["Zephyr"], - "date of birth": ["2015-01-01"], - "city": ["Guelph"], - "postal code": ["N1H 2T2"], - "province/territory": ["ON"], - "overdue disease": ["Measles"], - "imms given": [""], - "street address line 1": ["123 Main"], - "street address line 2": [""], - } - ) - - result = preprocess.normalize_dataframe(df) - - # Should not raise error and column names should be normalized - assert len(result) == 1 - - def test_normalize_dataframe_missing_required_raises_error(self) -> None: - """Verify error when required columns are missing. - - Real-world significance: - - Missing critical columns (e.g., OVERDUE DISEASE) means input is invalid - - Must fail early with clear error - """ - df = pd.DataFrame( - { - "SCHOOL NAME": ["Test"], - "CLIENT ID": ["C001"], - # Missing required columns - } - ) - - with pytest.raises(ValueError, match="Missing required columns"): - preprocess.normalize_dataframe(df) - def test_normalize_dataframe_handles_missing_values(self) -> None: - """Verify NaN/None values are converted to empty strings. - - Real-world significance: - - Input may have missing fields (e.g., no suite number) - - Must normalize to empty strings for consistent processing - """ - df = sample_input.create_test_input_dataframe(num_clients=3) - # Normalize column names first, then inject NaN to test fill behavior - working = preprocess.normalize_dataframe(df) - working.loc[0, "STREET_ADDRESS_LINE_2"] = None - working.loc[1, "POSTAL_CODE"] = float("nan") + """Verify NaN/None values in string columns are filled.""" + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) + df.loc[0, "STREET_ADDRESS_LINE_2"] = None + df.loc[1, "POSTAL_CODE"] = float("nan") - result = preprocess.normalize_dataframe(working) + result = preprocess.normalize_dataframe(df) assert result["STREET_ADDRESS_LINE_2"].iloc[0] == "" assert result["POSTAL_CODE"].iloc[1] == "" def test_normalize_dataframe_converts_dates(self) -> None: - """Verify dates are converted to datetime objects. - - Real-world significance: - - Date fields must be parsed for age calculation - - Invalid dates must be detected early - """ - df = sample_input.create_test_input_dataframe(num_clients=2) - df["DATE OF BIRTH"] = ["2015-01-02", "2014-05-06"] + """Verify DATE_OF_BIRTH is parsed to datetime.""" + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) + df["DATE_OF_BIRTH"] = ["2015-01-02", "2014-05-06"] result = preprocess.normalize_dataframe(df) assert pd.api.types.is_datetime64_any_dtype(result["DATE_OF_BIRTH"]) def test_normalize_dataframe_trims_whitespace(self) -> None: - """Verify string columns have whitespace trimmed. - - Real-world significance: - - Input may have accidental leading/trailing spaces - - Must normalize for consistent matching - """ - df = sample_input.create_test_input_dataframe(num_clients=1) - df["FIRST NAME"] = [" Alice "] - df["LAST NAME"] = [" Zephyr "] + """Verify string columns have leading/trailing whitespace stripped.""" + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df["FIRST_NAME"] = [" Alice "] + df["LAST_NAME"] = [" Zephyr "] result = preprocess.normalize_dataframe(df) @@ -544,7 +392,7 @@ def test_build_result_generates_clients_with_sequences( - Sequence numbers (00001, 00002...) appear on notices - Must be deterministic: same input → same sequences """ - df = sample_input.create_test_input_dataframe(num_clients=3) + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) result = preprocess.build_preprocess_result( df, @@ -568,7 +416,7 @@ def test_build_result_sorts_clients_deterministically( - Required for comparing pipeline runs (reproducibility) - Enables batching by school to work correctly """ - df = sample_input.create_test_input_dataframe(num_clients=3) + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) result1 = preprocess.build_preprocess_result( df, @@ -598,37 +446,26 @@ def test_build_result_sorts_by_school_then_name( - Must be deterministic across pipeline runs - Affects sequence number assignment """ - df = pd.DataFrame( + df = preprocess.map_columns(pd.DataFrame( { - "SCHOOL NAME": [ - "Zebra School", - "Zebra School", - "Apple School", - "Apple School", - ], - "CLIENT ID": ["C002", "C001", "C004", "C003"], - "FIRST NAME": ["Bob", "Alice", "Diana", "Chloe"], - "LAST NAME": ["Smith", "Smith", "Jones", "Jones"], - "DATE OF BIRTH": [ - "2015-01-01", - "2015-01-02", - "2015-01-03", - "2015-01-04", - ], - "CITY": ["Town", "Town", "Town", "Town"], - "POSTAL CODE": ["N1H 2T2", "N1H 2T2", "N1H 2T2", "N1H 2T2"], - "PROVINCE/TERRITORY": ["ON", "ON", "ON", "ON"], - "OVERDUE DISEASE": ["Measles", "Measles", "Measles", "Measles"], - "IMMS GIVEN": ["", "", "", ""], - "STREET ADDRESS LINE 1": [ - "123 Main", - "123 Main", - "123 Main", - "123 Main", - ], - "STREET ADDRESS LINE 2": ["", "", "", ""], + "School Type": ["Public", "Public", "Public", "Public"], + "School Name": ["Zebra School", "Zebra School", "Apple School", "Apple School"], + "Client Id": ["C002", "C001", "C004", "C003"], + "First Name": ["Bob", "Alice", "Diana", "Chloe"], + "Last Name": ["Smith", "Smith", "Jones", "Jones"], + "Age": ["10", "10", "10", "10"], + "Date of Birth": ["2015-01-01", "2015-01-02", "2015-01-03", "2015-01-04"], + "Street Address Line 1": ["123 Main", "123 Main", "123 Main", "123 Main"], + "Street Address Line 2": ["", "", "", ""], + "City": ["Town", "Town", "Town", "Town"], + "Province/Territory": ["ON", "ON", "ON", "ON"], + "Postal Code": ["N1H 2T2", "N1H 2T2", "N1H 2T2", "N1H 2T2"], + "Overdue Disease": ["Measles", "Measles", "Measles", "Measles"], + "Overdue Agent": ["MMR", "MMR", "MMR", "MMR"], + "Imms Given": ["", "", "", ""], + "Birth Year": ["2015", "2015", "2015", "2015"], } - ) + )) result = preprocess.build_preprocess_result( df, language="en", @@ -651,8 +488,8 @@ def test_build_result_maps_vaccines_correctly( - Vaccine mapping must preserve all components - Affects disease coverage reporting in notices """ - df = sample_input.create_test_input_dataframe(num_clients=1) - df["IMMS GIVEN"] = ["May 1, 2020 - DTaP"] + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df["IMMS_GIVEN"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( df, @@ -691,9 +528,9 @@ def test_build_result_uses_explicit_config_path( ), encoding="utf-8", ) - df = sample_input.create_test_input_dataframe(num_clients=1) - df["OVERDUE DISEASE"] = ["DTaP - 2"] - df["IMMS GIVEN"] = ["May 1, 2020 - DTaP"] + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df["OVERDUE_DISEASE"] = ["DTaP - 2"] + df["IMMS_GIVEN"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( df, @@ -719,22 +556,26 @@ def test_build_result_handles_missing_board_name_with_warning( - Should auto-generate board ID and log warning - Allows pipeline to proceed without failing """ - df = pd.DataFrame( + df = preprocess.map_columns(pd.DataFrame( { - "SCHOOL NAME": ["Test School"], - "CLIENT ID": ["C001"], - "FIRST NAME": ["Alice"], - "LAST NAME": ["Zephyr"], - "DATE OF BIRTH": ["2015-01-01"], - "CITY": ["Guelph"], - "POSTAL CODE": ["N1H 2T2"], - "PROVINCE/TERRITORY": ["ON"], - "OVERDUE DISEASE": ["Measles"], - "IMMS GIVEN": [""], - "STREET ADDRESS LINE 1": ["123 Main"], - "STREET ADDRESS LINE 2": [""], + "School Type": ["Public"], + "School Name": ["Test School"], + "Client Id": ["C001"], + "First Name": ["Alice"], + "Last Name": ["Zephyr"], + "Age": ["10"], + "Date of Birth": ["2015-01-01"], + "Street Address Line 1": ["123 Main"], + "Street Address Line 2": [""], + "City": ["Guelph"], + "Province/Territory": ["ON"], + "Postal Code": ["N1H 2T2"], + "Overdue Disease": ["Measles"], + "Overdue Agent": ["MMR"], + "Imms Given": [""], + "Birth Year": ["2015"], } - ) + )) result = preprocess.build_preprocess_result( df, language="en", @@ -756,7 +597,7 @@ def test_build_result_french_language_support( - Preprocessing must handle both language variants - Dates must convert to French format for display """ - df = sample_input.create_test_input_dataframe(num_clients=1, language="fr") + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1, language="fr")) result = preprocess.build_preprocess_result( df, @@ -777,7 +618,7 @@ def test_build_result_handles_replace_unspecified( - Input may contain "Not Specified" vaccine agents - Pipeline should filter these out to avoid confusing notices """ - df = sample_input.create_test_input_dataframe(num_clients=1) + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) result = preprocess.build_preprocess_result( df, @@ -798,10 +639,10 @@ def test_build_result_detects_duplicate_client_ids( - Must warn about this data quality issue - Later records with same ID will overwrite earlier ones in notice generation """ - df = sample_input.create_test_input_dataframe(num_clients=2) + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) # Force duplicate client IDs - df.loc[0, "CLIENT ID"] = "C123456789" - df.loc[1, "CLIENT ID"] = "C123456789" + df.loc[0, "CLIENT_ID"] = "C123456789" + df.loc[1, "CLIENT_ID"] = "C123456789" result = preprocess.build_preprocess_result( df, @@ -829,13 +670,13 @@ def test_build_result_detects_multiple_duplicate_client_ids( - May have multiple different client IDs that are duplicated - Each duplicate set should generate a separate warning """ - df = sample_input.create_test_input_dataframe(num_clients=5) + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=5)) # Create two sets of duplicates - df.loc[0, "CLIENT ID"] = "C111111111" - df.loc[1, "CLIENT ID"] = "C111111111" - df.loc[2, "CLIENT ID"] = "C111111111" - df.loc[3, "CLIENT ID"] = "C222222222" - df.loc[4, "CLIENT ID"] = "C222222222" + df.loc[0, "CLIENT_ID"] = "C111111111" + df.loc[1, "CLIENT_ID"] = "C111111111" + df.loc[2, "CLIENT_ID"] = "C111111111" + df.loc[3, "CLIENT_ID"] = "C222222222" + df.loc[4, "CLIENT_ID"] = "C222222222" result = preprocess.build_preprocess_result( df, @@ -866,7 +707,7 @@ def test_build_result_no_warning_for_unique_client_ids( Real-world significance: - Normal case with clean data should not produce duplicate warnings """ - df = sample_input.create_test_input_dataframe(num_clients=3) + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) result = preprocess.build_preprocess_result( df, @@ -1379,7 +1220,7 @@ def test_two_vaccines_sharing_disease_column_produces_mixed(self) -> None: header = ["Diphtheria", "Other"] rows = preprocess.build_received_rows( "May 1, 2020 - VaxA - Valid; May 1, 2020 - VaxB - Invalid", - [], ref, header, + [], ref, header, show_validity_markers=True, ) assert len(rows) == 2 assert rows[0]["date_rowspan"] == 2 @@ -1395,7 +1236,7 @@ def test_date_rowspan_on_continuation_rows_is_zero(self) -> None: header = ["Diphtheria", "Other"] rows = preprocess.build_received_rows( "May 1, 2020 - VaxA - Valid; May 1, 2020 - VaxB - Invalid", - [], ref, header, + [], ref, header, show_validity_markers=True, ) assert rows[1]["date_rowspan"] == 0 @@ -1403,7 +1244,7 @@ def test_other_column_mixed_triggers_split(self, vaccine_ref, header) -> None: """HBV(valid) + HPV(invalid) both map to Other → Other is mixed → split.""" rows = preprocess.build_received_rows( "May 1, 2020 - HBV - Valid; May 1, 2020 - HPV - Invalid", - [], vaccine_ref, header, + [], vaccine_ref, header, show_validity_markers=True, ) assert len(rows) == 2 assert rows[0]["columns"].get("Other") == "valid" @@ -1449,8 +1290,8 @@ def test_build_result_maps_vaccines_correctly(self, default_vaccine_reference) - Real-world significance: - DTaP → Diphtheria, Tetanus, Pertussis columns populated. """ - df = sample_input.create_test_input_dataframe(num_clients=1) - df["IMMS GIVEN"] = ["May 1, 2020 - DTaP"] + df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df["IMMS_GIVEN"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( df, diff --git a/uv.lock b/uv.lock index ec41ea1..a79a5ff 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,33 @@ resolution-markers = [ "python_full_version < '3.11'", ] +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "babel" version = "2.18.0" @@ -47,6 +74,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "chardet" +version = "7.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/cd61c567092a6cec796144510a68aff158ebfc1df82950a45bae65f28413/chardet-7.6.0.tar.gz", hash = "sha256:93d9df6089ded42ed1fe9f57e272c0b74bd0464d45c0c7d50f09f26f31105c3c", size = 914462, upload-time = "2026-08-14T20:36:59.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/cd/dc9df6b57c043037920a3896ccb08a21e0595fe592b7acb9fc85acaf0698/chardet-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbaca8f563a9de07ab1a53157dba93802e54c26afe3339892afcc7c59ea4ef1b", size = 1093508, upload-time = "2026-08-14T20:36:07.451Z" }, + { url = "https://files.pythonhosted.org/packages/b9/66/acab13c5bbe55410273530a41b83fdcde0b653f6bded34702504c0ab95cb/chardet-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55a4c31adc7c7e83ad412f2f66b6b7358d0d4fe67505e7f58e18f68f75d341bb", size = 1074662, upload-time = "2026-08-14T20:36:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/20/38/b32daa70f8bacd47e515ec9f85d571c87f31ff1c43c95aea7780eaefe7fc/chardet-7.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b586cab9e9072dddd89bc2bd27ee72808d0c84ec73695fe6ec0f3c46b057c65", size = 1465648, upload-time = "2026-08-14T20:36:10.774Z" }, + { url = "https://files.pythonhosted.org/packages/f2/34/0fd9d566df9647d0820a35b4d119db971da3ae9be20899d2b4af11d7ab58/chardet-7.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b81d3f7d7914442d5f7d515b8c6d79cee6b794bc208971fb6902f176671166a", size = 1484628, upload-time = "2026-08-14T20:36:12.456Z" }, + { url = "https://files.pythonhosted.org/packages/ad/be/1c57863e21c1d6cb6b0fcdde313f8a24039eca01cc90ef0f0986c0964969/chardet-7.6.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dde4080fb6bb8db96e8c44893771bcc0d235f4c22cdddb194a765a65e3a72ba7", size = 1449178, upload-time = "2026-08-14T20:36:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/70/22/98d824fe8bce808168dd364e843ccee858bb709e7e72c9b8a60f8d5201e4/chardet-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6030886e7da2740bf299b6a8cc75b4dcc2c90db0ca8fe0a6e4fd0bfd071dabd", size = 1158522, upload-time = "2026-08-14T20:36:15.858Z" }, + { url = "https://files.pythonhosted.org/packages/11/2e/d8634bee23a07bf512512ddf6218a68e47f045ae061c0cc80657ed79dcc0/chardet-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6424512f576fa7e88b7431d38a42d57552c8f717465a975fc42e497cd280d833", size = 1088243, upload-time = "2026-08-14T20:36:17.135Z" }, + { url = "https://files.pythonhosted.org/packages/55/95/bd6d59026638cec47dace85858171fbecadd2f9e58cb2b973dc515aa790c/chardet-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:284136186ff90735f901ed0a1c6d41e7af67c666841cc0eceb58482a21b7056c", size = 1068688, upload-time = "2026-08-14T20:36:19.183Z" }, + { url = "https://files.pythonhosted.org/packages/7f/4a/60ed03656b28c1f4d378bc3cfe8a6cdda62c7c289398f925610fbbabfd00/chardet-7.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e9b31b9ae93872d66439b046a1e08c2ea99791f3c254dce1e2633e395c5587c", size = 1487945, upload-time = "2026-08-14T20:36:20.64Z" }, + { url = "https://files.pythonhosted.org/packages/03/25/9c8db4f951e974a4db5558d9eea62e1fd5b5889c9d0ad0315eed67c5cdb3/chardet-7.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa03322e07ac08d520ec50bb50c73143d0892d1adc067d4c5e58f4ef4b2363a8", size = 1510112, upload-time = "2026-08-14T20:36:22.248Z" }, + { url = "https://files.pythonhosted.org/packages/76/1b/59eb88a78d8f5855c27c25788088df82834ad067f3dddaf4d86ef03cf2d3/chardet-7.6.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ad9bc6dab4f338673353fa3f0dc96122f559aaf746087408106e2fcbf132fe8", size = 1462470, upload-time = "2026-08-14T20:36:23.557Z" }, + { url = "https://files.pythonhosted.org/packages/02/af/46c80c317f9b4dd61f15c33b1e073fbdd64d13c70fe19e34943071dc9e50/chardet-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:360260d074d8712ac1e9048fcafb0fdde246f9d0b12555748ad0017c5ecee43d", size = 1157092, upload-time = "2026-08-14T20:36:25.082Z" }, + { url = "https://files.pythonhosted.org/packages/6f/62/64da80dad0c804e743b4156f379183578f1e33918856ae928dc9248a6002/chardet-7.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19fea52164e6e00f2a21ed418f42e4b0162a09199274c86d07ad3efd661317c4", size = 1094073, upload-time = "2026-08-14T20:36:26.53Z" }, + { url = "https://files.pythonhosted.org/packages/44/99/934fb862d102c8756008597f4398323f32cef329f16e87fbb3bf76d4f4be/chardet-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a12023d48d0e207791c01161d03cb3c0d85c6a15f345eb9d3d56063a63d1e40f", size = 1071612, upload-time = "2026-08-14T20:36:28.067Z" }, + { url = "https://files.pythonhosted.org/packages/71/e9/b04e0ec576a77e79fe37279a9a5d5b1ae752d365e43df2eca0d0eee4cea5/chardet-7.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:249993b88ac7a58cad2781acea8f379152a28a719c9b401d614898c63a8c83da", size = 1489219, upload-time = "2026-08-14T20:36:29.357Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a2/c4d99299e9ce7fad561f8bb56babbbbdd3bb6b4fbd7c0ec674c1dbdd2cc5/chardet-7.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf0adaca8b1c4bacfade9d0a1e4f8f70b1bb122833d6f07ab90e3adc84eb13a", size = 1518293, upload-time = "2026-08-14T20:36:30.879Z" }, + { url = "https://files.pythonhosted.org/packages/56/1d/49f13052b74303bab2789d098063cbd19758217949ea54ffa216b6098cb3/chardet-7.6.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf6d08c2373b7772a558d141f9e8cee53fe1d222341bac612e4d558b04995f73", size = 1459077, upload-time = "2026-08-14T20:36:32.149Z" }, + { url = "https://files.pythonhosted.org/packages/0d/53/8da1f4758286efd8faf71356facddb382788ecf1bbd7c70d63e2e18a4898/chardet-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:406936df1328a3284fef366eaa2bfd1cccd0ef1b10cb99781dd5b022ea644b84", size = 1160778, upload-time = "2026-08-14T20:36:33.436Z" }, + { url = "https://files.pythonhosted.org/packages/a3/29/16a7419edfbd60e901e6a797cbc3e038cb2a81903bc16c029db755f0156f/chardet-7.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:57e6846cc13ce1ff59979f4ec9da770c57e12aa99046073f632de5a51d9a6f20", size = 1088449, upload-time = "2026-08-14T20:36:34.723Z" }, + { url = "https://files.pythonhosted.org/packages/1d/36/3a14b0f8ddeb302f157281ca656a3ce6874b78e2d6af03682f520b487245/chardet-7.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:089e3bb81a0a07e94f15461ded9f9ee66d349615b1a9fd557d4de1003e2fc12e", size = 1065126, upload-time = "2026-08-14T20:36:36.21Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4c/f59a39c2bfe4ac99baba8da842e8d2ea0b84dff7ba53a96e7ad8c71602d7/chardet-7.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43ea433e43a23c55e8e17f3fad1e07f5cfe5450c73124b95b0d849c21ad379ee", size = 1482492, upload-time = "2026-08-14T20:36:37.649Z" }, + { url = "https://files.pythonhosted.org/packages/bd/eb/93e8036681157f2217a18769927a035984526e6dbd5e91f28a375ca41c14/chardet-7.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b5d31f9b7f793e15e81cca877e7ccd72bffffa2a3443a9d47be9dfee84fad69", size = 1514185, upload-time = "2026-08-14T20:36:39.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/04/0066d7ab2c135e404a6fa166bb7fa49d1c7bf7af07b86ed95b1c48a348c7/chardet-7.6.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c54b6a8d3b219560fa5cf4c28df932c37471afe047afdc152067104e741f38c1", size = 1454420, upload-time = "2026-08-14T20:36:40.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9f/e965f1d9eddfb86cf118f980d329cbee43c4ac4b562448b369a6b6ef36af/chardet-7.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:b3b4c96c4df93899b3c8b9e8159e06b1f55c66d7ca384d91481108e251a06eb0", size = 1159067, upload-time = "2026-08-14T20:36:41.816Z" }, + { url = "https://files.pythonhosted.org/packages/cf/78/e14991c9487277ed7d006fff58f3084ac792ed94cced081788abb95df70c/chardet-7.6.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6061adf247ab5dda173b67010e13904c6071717660c7c8077fb50aca362b264", size = 1087678, upload-time = "2026-08-14T20:36:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/0f95e04cb1820e0a582cd6d86bbf26be8302a94ccf330f8ba5f69735389d/chardet-7.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fc1e1571321baf8927582fe34363ad7f02279f11c8c2839c14b4c76894148db6", size = 1065310, upload-time = "2026-08-14T20:36:44.489Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a1/04404dcc9d6e1253b02583e562d2c7ddca50a7e91f460d62eff7e1d07c92/chardet-7.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8900f6c7cf6b015b17a51767cc6144689059ba1cdceaa383d29eb037ac28579e", size = 1485028, upload-time = "2026-08-14T20:36:45.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/88/360064c4c7d9d0664561dae03b74c871d2f5332b329f5c99f1c997fb869a/chardet-7.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cedbc584789eb2edfde20fd03669972a833ce6019e60014ae613f9bfc440e8e3", size = 1510242, upload-time = "2026-08-14T20:36:47.206Z" }, + { url = "https://files.pythonhosted.org/packages/43/4c/302869fa1c69a5a41e4e78782680b12552ffd4286c3ae3c839b7b8df53d4/chardet-7.6.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5dc835e40e0e09c2c3eab43731a8b5127834f42786dda09ba2f4b699ccd527a", size = 1456456, upload-time = "2026-08-14T20:36:48.668Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/ff4fef15ed25fc3f945a3b981ae0f43c8559b3fbedb40267e59e583d105b/chardet-7.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:0f304de7041afaec0195ad6464937cd112392002e9d72ed15d55f20a9abd3a13", size = 1159103, upload-time = "2026-08-14T20:36:50.107Z" }, + { url = "https://files.pythonhosted.org/packages/31/44/94e6f89485ce6c630e8fec6d388e3fa747e58b7246b0cc8c5c532ba98650/chardet-7.6.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a4f0a368ad04d5def08bdfaa17c7e15e71552f93923dc2aa9b2f7d9dee02fbb6", size = 1120302, upload-time = "2026-08-14T20:36:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/e0/cd/8e68ba12f13aaf4ee82d54ef8a1f83d62942e7a6bc1e579dd2d530a3e26e/chardet-7.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:75d6c3a4d2046d49e83d2d2206eb073a1f390743e856d90c1bbc19949b26acf4", size = 1093157, upload-time = "2026-08-14T20:36:52.987Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f6/6a35342b9efa69dcceab0ea8966571c6442a59c336bf460f0a2af95ed234/chardet-7.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:459e2b1c98f9a86a4698112aa42dffa802bbbff883c1ff144071f87224125862", size = 1521213, upload-time = "2026-08-14T20:36:54.812Z" }, + { url = "https://files.pythonhosted.org/packages/df/8c/8f09bfabbaedae45caa72b996e96d5e3f6436dfddc55c1311ffa2f6bd7d2/chardet-7.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bdb6f03107b7ace3f44e0edd91aa24456ee558787df265cc19daf45785b31c7", size = 1528427, upload-time = "2026-08-14T20:36:56.224Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3d/2540627b193112e08b8045f3cad9295570866208555aa6625b63cc69c6cd/chardet-7.6.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:0c44a32da32cc8b23d6b20d98ace15ec7600950e4955d1bf5ab1f849b0187fdb", size = 1087374, upload-time = "2026-08-14T21:04:14.994Z" }, + { url = "https://files.pythonhosted.org/packages/fe/de/dac4f550cde73c4732b4916e72ec489ae36933fbbd1697e4122d3ae2e9dd/chardet-7.6.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7bbc8a9652c7f859c593847f220c1d264f25749369abb1a267b404ee8cceb209", size = 1064688, upload-time = "2026-08-14T21:04:16.806Z" }, + { url = "https://files.pythonhosted.org/packages/06/45/f4f3f288496797d2cf1d1e9036f920ed2b4c79787b21c42a3314f4a6d532/chardet-7.6.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83512a475a2f3886166aa0bca1bbb39343a4eb3186dd5532127d6f2591d09118", size = 1486320, upload-time = "2026-08-14T21:04:18.535Z" }, + { url = "https://files.pythonhosted.org/packages/c0/81/3ca30c16e6c6015b737fca22d9342957cc617d8a65329d3e963ad754a31b/chardet-7.6.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:271ab71ec1be61dbbce0436de0848895c03eae051c379e937a39573d9ce403d9", size = 1515374, upload-time = "2026-08-14T21:04:20.724Z" }, + { url = "https://files.pythonhosted.org/packages/6e/98/163a75b8b3372a6b6503f5f5a4154a222ac135c7a706b5d176f8e4b237a2/chardet-7.6.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da86fc1b40ff5996fbb5e4c2d2dca770eac2c893cef157dacc050b8b4d929846", size = 1469504, upload-time = "2026-08-14T21:04:22.352Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4f/3ad1b1bd27ae7f0d3d13cf711366525cd781a9e067950d8a09aa280916cb/chardet-7.6.0-cp315-cp315-win_amd64.whl", hash = "sha256:b73f277c1ac09c4f8076c4214b816c7aa78a0a2f0cb7156742f4303f856bedc3", size = 1158551, upload-time = "2026-08-14T21:04:24.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/92/22a609c68c5123ebdccd8fe1a80e423609012ce20166de19a2ed3955b2f7/chardet-7.6.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:61238d5945b36af9a2ad13494f8969b7deb3c3b4abe223e54670c064e73f5328", size = 1119186, upload-time = "2026-08-14T21:04:25.761Z" }, + { url = "https://files.pythonhosted.org/packages/29/35/75a90143e4200e197f4f6cf5895d379e22bc785d0c7711120df18fa5347c/chardet-7.6.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f2ec3c78cc6b54bf8e091ec4ee885473078b5d7ef18ab1b01c86ae1e98bf88f7", size = 1092596, upload-time = "2026-08-14T21:04:27.884Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2c/b6d5f47d878c46e04ae6fcb58e0969467925bc0819b333c682e0c41bbc5c/chardet-7.6.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:167d7ba3ee08b654e36d7b43ebd9a36606c9a12e2fabdb361757a095ca3b7e3d", size = 1516644, upload-time = "2026-08-14T21:04:29.624Z" }, + { url = "https://files.pythonhosted.org/packages/25/d2/2bc3f1066c6f27bc3fa3a98dfd8a2a9c1e969a9d6bdc1edcd35278791fc4/chardet-7.6.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f14f46ef1977e41ce1f4814ca6984cea7f8b6baf8cbc6626ef7bf3d13cf7ea13", size = 1533221, upload-time = "2026-08-14T21:04:31.39Z" }, + { url = "https://files.pythonhosted.org/packages/cf/6e/5a0b348fa4cd7847567a28c6e697ccf58391960bfd13a6e7473ee23ca2f2/chardet-7.6.0-py3-none-any.whl", hash = "sha256:4076d795897ce45239825956a1334e134322ecc4bfe84dbb12acd5390de0fbc1", size = 680279, upload-time = "2026-08-14T20:36:57.763Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.9" @@ -298,6 +378,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] +[[package]] +name = "frictionless" +version = "5.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "chardet" }, + { name = "humanize" }, + { name = "isodate" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "marko" }, + { name = "petl" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rfc3986" }, + { name = "simpleeval" }, + { name = "tabulate" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "validators" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/21/3494557b8c72b8375597cdd5b85d46c76eb978994c2f34f5737262047438/frictionless-5.19.0.tar.gz", hash = "sha256:75a060c8806c4a84dd72112ceeb919277d84982b07420295b9b920e8e30a65ab", size = 74375968, upload-time = "2026-04-13T13:05:53.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/3a/e568ea129fbcf9ee0f148178c67f8a2fc4e88de3aa34c840a180bcce4775/frictionless-5.19.0-py3-none-any.whl", hash = "sha256:81436b94a950b1a5cf83e953921e64dc888c00f51400f783ea0aa824319a6d67", size = 535173, upload-time = "2026-04-13T13:05:50.897Z" }, +] + +[package.optional-dependencies] +excel = [ + { name = "openpyxl" }, + { name = "tableschema-to-template" }, + { name = "xlrd" }, + { name = "xlwt" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -336,6 +454,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] +[[package]] +name = "humanize" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/ea/13a1ef3c12d12662905801495283530251918b70d62d368f1d2e0272c70d/humanize-4.16.0.tar.gz", hash = "sha256:7dc2244a2f84a4bfb1d36c37bac80cd78e35cdc5c119206d87b018e1445f3a3f", size = 89515, upload-time = "2026-06-30T16:17:29.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/aa/0b7365d30fed43e7a3449aba1fe20a0a7174d9cf13e282af4e69ac825441/humanize-4.16.0-py3-none-any.whl", hash = "sha256:353eb2f34c09d098b2880eee8bef21832eae6d174f48c5762fff7e5fcb74d01d", size = 137209, upload-time = "2026-06-30T16:17:28.36Z" }, +] + [[package]] name = "identify" version = "2.6.16" @@ -360,13 +487,13 @@ version = "1.0.0" source = { editable = "." } dependencies = [ { name = "babel" }, + { name = "frictionless", extra = ["excel"] }, { name = "openpyxl" }, { name = "pandas" }, { name = "pillow" }, { name = "pypdf" }, { name = "pyyaml" }, { name = "qrcode" }, - { name = "rapidfuzz" }, ] [package.dev-dependencies] @@ -391,13 +518,13 @@ docs = [ [package.metadata] requires-dist = [ { name = "babel", specifier = ">=2.17.0" }, + { name = "frictionless", extras = ["excel", "csv"] }, { name = "openpyxl" }, { name = "pandas" }, { name = "pillow", specifier = ">=12.3.0" }, { name = "pypdf" }, { name = "pyyaml" }, { name = "qrcode", specifier = ">=7.4.2" }, - { name = "rapidfuzz" }, ] [package.metadata.requires-dev] @@ -428,6 +555,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -440,6 +576,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "markdown" version = "3.10.3" @@ -449,6 +613,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "marko" +version = "2.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/6d/671a18bb386adca6311bca6c2fe6bec873947ee501cc5f8f509ec06bdde0/marko-2.2.4.tar.gz", hash = "sha256:c042c66f835425673123d7536b39b4660de3b68e30078c70fd26245b31170683", size = 151013, upload-time = "2026-08-12T03:20:11.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/78/751d49c8bdfa0b30147c99235516433af6b63eca367ff28593c7346a0494/marko-2.2.4-py3-none-any.whl", hash = "sha256:d80510506edba096ec49d4720a09645fa0bb78e7b7b88697f20032fc19730aa9", size = 46753, upload-time = "2026-08-12T03:20:10.811Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -534,6 +719,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +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 = "mergedeep" version = "1.3.4" @@ -916,6 +1110,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "petl" +version = "1.7.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/be/e24c38649619c531f76e492cc0c329099bc567777e094c181205066ba76e/petl-1.7.23.tar.gz", hash = "sha256:b5c54f281b761f983ca6a4d8bc54fbd7b8285816a03ac0534552bee873c1e655", size = 434014, upload-time = "2026-08-05T00:20:39.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/71/c0a90cfc4df1eed36d634d88c93bdc3583eee792f999ec4f03b584cf1d77/petl-1.7.23-py3-none-any.whl", hash = "sha256:23200769b0bfc9cdde2866f3fe34991a2e465de9c173018683eddcf41b8cdc77", size = 239184, upload-time = "2026-08-05T00:20:38.342Z" }, +] + [[package]] name = "pillow" version = "12.3.0" @@ -1044,6 +1247,137 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" }, ] +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + [[package]] name = "pygments" version = "2.21.0" @@ -1131,6 +1465,18 @@ 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]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -1229,93 +1575,18 @@ wheels = [ ] [[package]] -name = "rapidfuzz" -version = "3.14.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/21/ef6157213316e85790041254259907eb722e00b03480256c0545d98acd33/rapidfuzz-3.14.5.tar.gz", hash = "sha256:ba10ac57884ce82112f7ed910b67e7fb6072d8ef2c06e30dc63c0f604a112e0e", size = 57901753, upload-time = "2026-04-07T11:16:31.931Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/b1/d6d6e7737fe3d0eb2ac2ac337686420d538f83f28495acc3cc32201c0dbf/rapidfuzz-3.14.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:071d96b957a33b9296b9284b6350a0fb6d030b154a04efd7c15e56b98b79a517", size = 1953508, upload-time = "2026-04-07T11:13:37.733Z" }, - { url = "https://files.pythonhosted.org/packages/2b/7b/94c1c953ac818bdd88b43213a9d38e4a41e953b786af3c3b2444d4a8f96d/rapidfuzz-3.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667f40fe9c81ad129b198d236881b00dd9e8314d9cc72d03c3e16bdfe5879051", size = 1160895, upload-time = "2026-04-07T11:13:39.278Z" }, - { url = "https://files.pythonhosted.org/packages/7f/60/a67a7ca7c2532c6c1a4b5cd797917780eed43798b82c98b6df734a086c95/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9fff308486bbd2c8c24f25e8e152c7594d3fe8db265a2d6a1ce24d58671127f", size = 1382245, upload-time = "2026-04-07T11:13:41.054Z" }, - { url = "https://files.pythonhosted.org/packages/95/ff/a42c9ce9f9e90ceb5b51136e0b8e8e6e5113ba0b45d986effbd671e7dddf/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dfa552338f51aec280f17b02d28bace1e162d1a84ccd80e3339a57f98aedb56b", size = 3163974, upload-time = "2026-04-07T11:13:42.662Z" }, - { url = "https://files.pythonhosted.org/packages/e3/3c/11e2d41075e6e48b7dad373631b379b7e40491f71d5412c5a98d3c58f60f/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:068b3e965ca9d9ee4debe40001ae7c3938ba646308afd33cf0c66618147db65c", size = 1475540, upload-time = "2026-04-07T11:13:44.687Z" }, - { url = "https://files.pythonhosted.org/packages/29/fa/09be143dcc22c79f09cf90168a574725dbda49f02cbbd55d0447da8bec86/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88b7d31ff1cc5e9bc0e4406e6b1fa00b6d37163d50bb58091e9b976ff1129faa", size = 2404128, upload-time = "2026-04-07T11:13:46.641Z" }, - { url = "https://files.pythonhosted.org/packages/32/f9/1aeb504cdcfde42881825e9c86f48238d4e01ba8a1530491e82eb17e5689/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eacb434410b8d9ca99a8d42352ef085cf423e3c76c1f0b86be2fcba3bff2952c", size = 2508455, upload-time = "2026-04-07T11:13:48.726Z" }, - { url = "https://files.pythonhosted.org/packages/10/8e/b1b5eed8d887a29b0e18fd3222c46ca60fddfb528e7e1c41267ce42d5522/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:649712823f3abcdc48427147a5384fac15623ba435d0013959b52e6462521397", size = 4274060, upload-time = "2026-04-07T11:13:50.805Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c4/7e5b0353693d4f47b8b0f96e941efc377cfb2034b67ef92d082ac4441a0f/rapidfuzz-3.14.5-cp310-cp310-win32.whl", hash = "sha256:13cb79c23ef5516e4c4e3830877be8b19aa75203636be1163d690d37803f6504", size = 1727457, upload-time = "2026-04-07T11:13:52.45Z" }, - { url = "https://files.pythonhosted.org/packages/d9/6e/f530a39b946fa71c009bc9c81fdb6b48a77bbc57ee8572ac0302b3bf6308/rapidfuzz-3.14.5-cp310-cp310-win_amd64.whl", hash = "sha256:f2073495a7f9b75e57e600747ac09510d67683fd64d3228e009740b7ef88f9fe", size = 1544657, upload-time = "2026-04-07T11:13:54.952Z" }, - { url = "https://files.pythonhosted.org/packages/bc/01/02fa075f9f59ff766d374fecbd042b3ac9782dcd5abc52d909a54f587eeb/rapidfuzz-3.14.5-cp310-cp310-win_arm64.whl", hash = "sha256:8166efddea49fdbc61185559f47593239e4794fd7c9044dd5a789d1a90af852d", size = 816587, upload-time = "2026-04-07T11:13:56.418Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f9/3c41a7be8855803f4f6c713b472226a98d31d41869d98f64f4ca790510d6/rapidfuzz-3.14.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e251126d48615e1f02b4a178f2cd0cd4f0332b8a019c01a2e10480f7552554b4", size = 1952372, upload-time = "2026-04-07T11:13:58.32Z" }, - { url = "https://files.pythonhosted.org/packages/9e/89/c2557e37531d03465193bff0ab9de70b468420a807d71a26a65100635459/rapidfuzz-3.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ab449c9abd0d4e1f8145dce0798a4c822a1a1933d613c764a641bea88b8bdab", size = 1159782, upload-time = "2026-04-07T11:14:00.127Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b2/ffeeb7eca1a897d51b998f4c0ef0281696c3b06abcca4f88f9def708ffe1/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb2829fedd672dd7107267189dabe2bbe07972801d636014417c6861eb89e358", size = 1383677, upload-time = "2026-04-07T11:14:01.696Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d0/4539e42a2d596e068f7738f279638a4a74edd1fbb6f8594e2458058979c6/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d50e5861872935fece391351cbb5ba21d1bced277cf5e1143d207a0a35f1925", size = 3168906, upload-time = "2026-04-07T11:14:03.29Z" }, - { url = "https://files.pythonhosted.org/packages/5e/1c/3ec897eb9d8b05308aa8ef6ae4ed64b088ad521a3f9d8ff469e7e97bc2b0/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:7092a216728f80c960bd6b3807275d1ee318b168986bd5dc523349581d4890b8", size = 1478176, upload-time = "2026-04-07T11:14:04.94Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ba/970c03a12ce20a5399e22afe9f8932fd4cd1265b8a8461d0e63b00eb4eae/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9669753caef7fdc6529f6adcc5883ed98d65976445d9322e7dbdb6b697feee13", size = 2402441, upload-time = "2026-04-07T11:14:07.228Z" }, - { url = "https://files.pythonhosted.org/packages/81/93/61d351cae60c1d0e21ba5ff1a1015ad045539ed215da9d6e302204ed887a/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:823b1b9d9230809d8edcc18872770764bfe8ef4357995e16744047c8ccf0e489", size = 2511628, upload-time = "2026-04-07T11:14:09.234Z" }, - { url = "https://files.pythonhosted.org/packages/87/52/374d2d4f60fd98155142a869323aa221e30868cfa1f15171a0f64070c247/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f0b2af76b7e7060c09e1a0dfa9410eb19369cbe6164509bff2ef94094b54d2b6", size = 4275480, upload-time = "2026-04-07T11:14:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/d8/04/82e7989bc9ec20a15b720a335c5cb6b0724bf6582013898f90a3280cfccd/rapidfuzz-3.14.5-cp311-cp311-win32.whl", hash = "sha256:c5801a89604c65ab4cc9e91b23bc4076d0ca80efd8c976fb63843d7879a85d7f", size = 1725627, upload-time = "2026-04-07T11:14:13.217Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b5/eca8ac5609bc9bcb02bb6ff87fa5983cc92b8772d66a431556ab8a8c178f/rapidfuzz-3.14.5-cp311-cp311-win_amd64.whl", hash = "sha256:d7ca16637c0ede8243f84074044bd0b2335a0341421f8227c85756de2d18c819", size = 1545977, upload-time = "2026-04-07T11:14:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e1/dbf318de28f65fa2cdd0a9dfbdee380f8199eb83b19259bc4f8592551b4e/rapidfuzz-3.14.5-cp311-cp311-win_arm64.whl", hash = "sha256:8c90cdf8516d9057e502aa6003cea71cf5ec27cc44699ca52412b502a04761bb", size = 816827, upload-time = "2026-04-07T11:14:16.788Z" }, - { url = "https://files.pythonhosted.org/packages/d3/e3/574435c6aafb80254c191ef40d7aca2cb2bb97a095ec9395e9fa59ac307a/rapidfuzz-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d3378f471ef440473a396ce2f8e97ee12f89a78b495540e0a5617bbfe895638", size = 1944601, upload-time = "2026-04-07T11:14:18.771Z" }, - { url = "https://files.pythonhosted.org/packages/d0/1f/fbad3102a255ecc112ce9a7e779bacab7fd14398217be8868dc9082ba363/rapidfuzz-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e910eebca9fd0eba245c0555e764597e8a0cccb673a92da2dc2397050725f48", size = 1164293, upload-time = "2026-04-07T11:14:20.534Z" }, - { url = "https://files.pythonhosted.org/packages/88/37/a3eb7ff6121ed3a5f199a8c38cc86c8e481816f879cb0e0b738b078c9a7e/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01550fe5f60fd176aa66b7611289d46dc4aa4b1b904874c7b6d1d54e581c5ec1", size = 1371999, upload-time = "2026-04-07T11:14:22.63Z" }, - { url = "https://files.pythonhosted.org/packages/79/72/97a9728c711c7c1b06e107d3f0623880fb4ef90e147ed13c551a1730e7cc/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48bee0b91bebfaec41e1081e351000659ab7570cc4598d617aa04d5bf827f9e6", size = 3145715, upload-time = "2026-04-07T11:14:24.508Z" }, - { url = "https://files.pythonhosted.org/packages/ed/54/d5caabbea233ac90c286c87c260e49d7641467e87438a18d858e41c82e91/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:7e580cb04ad849ae9b786fa21383c6b994b6e6c1444ad1cb9f22392759d72741", size = 1456304, upload-time = "2026-04-07T11:14:26.515Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a7/2d1a81250ac8c01a0100c026018e76f0e7a097ff63e4c553e02a6938c6fb/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:09d6c9ba091854f07817055d795d604179c12a8f308ba4c7d56f3719dfea1646", size = 2389089, upload-time = "2026-04-07T11:14:28.635Z" }, - { url = "https://files.pythonhosted.org/packages/65/0d/c47c3872203ae88e6506997c0b576ad731f5261daa25d559be09c9756658/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1e989f86113be66574113b9c7bdf4793f3f863d248e47d911b355e05ca6b6b10", size = 2493404, upload-time = "2026-04-07T11:14:30.577Z" }, - { url = "https://files.pythonhosted.org/packages/8f/2f/71e0a5a3130792146c8a200a2dd1e52aa16f7c1074012e17f2601eea9a90/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ebd1a18e2e47bc0b292a07e6ed9c3642f8aaa672d12253885f599b50807a4f9", size = 4251709, upload-time = "2026-04-07T11:14:32.451Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/d39874901abacef325adb5b34ae416817c8486dfb4fb87c7a9b74ec5b072/rapidfuzz-3.14.5-cp312-cp312-win32.whl", hash = "sha256:9981d38a703b86f0e315a3cd229fd1906fe1d91c989ed121fb975b3c849f89f5", size = 1710069, upload-time = "2026-04-07T11:14:34.37Z" }, - { url = "https://files.pythonhosted.org/packages/85/0b/f65572c53de8a1c704bda707f63a447b67bdbe95d7cdc70d18885e191df5/rapidfuzz-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:d8375e3da319593389727c3187ccaf3e0e84199accc530866b8e0f2b79af05e9", size = 1540630, upload-time = "2026-04-07T11:14:36.287Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c3/143be3a578f989758cae516f3270d5cbb49783a7bfdf57cc27a670e00456/rapidfuzz-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:478b59bb018a6780d73f33e38d0b3ec5e968a6c1ed42876b993dd456b7aa20e8", size = 813137, upload-time = "2026-04-07T11:14:38.289Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/252803f2010ba699618cdc048b6e1f7cc1f433c08b4a9a17579b92ab0142/rapidfuzz-3.14.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd8fd343bf8492a1e60bcb6dc99f90f74f65d98d8241a6b3e1fed225b76ecd6", size = 1940205, upload-time = "2026-04-07T11:14:40.319Z" }, - { url = "https://files.pythonhosted.org/packages/ea/59/b2afd98e41af9cd54554a4c1c423d84cdd60e6b1c0a09496f033b55f60ec/rapidfuzz-3.14.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6737b35d5af7479c5bf9710f7b17edd9d2c43128d974d25fb4ea653e42c64609", size = 1159639, upload-time = "2026-04-07T11:14:42.52Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/7aa7e62c4c516a7af322ed0c4f0774208b72d457d0cfec808bad0df12f4a/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b002c7994cc9f2bc9d9856f0fbaee6e8072c983873846c92f25cefba5b2a925f", size = 1367194, upload-time = "2026-04-07T11:14:44.25Z" }, - { url = "https://files.pythonhosted.org/packages/90/79/2fc252a63bc91d3c3b234d0a3a6ad4ebc460037a23cdcdaf9285f986e6c9/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17a34330cd2a538c1ce5d400b61ba358c5b72c654b928ff87b362e88f8b864c7", size = 3151805, upload-time = "2026-04-07T11:14:46.21Z" }, - { url = "https://files.pythonhosted.org/packages/17/54/0c83508f2683ea70e2d05f8527eb07328acf7bb1e9d97a3bece5702378e7/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:95d937e74c1a7a1287dfb03b62a827be08ede10a155cf1af73bbf47f2b73ee6e", size = 1455667, upload-time = "2026-04-07T11:14:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/71/1b/070175e873177814d58850a01ebe80e20ae11e93eb4da894d563988660fa/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:46b92a9970dcc34f0096901c792644094cab49554ac3547f35e3aebbdf0a3610", size = 2388246, upload-time = "2026-04-07T11:14:50.098Z" }, - { url = "https://files.pythonhosted.org/packages/c9/dd/77caf7aaf9c2be050ad1f128d7c24ff0f59079aa62c5f62f9df41c0af45e/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e012177c8e8a8a0754ae0d6027d63042aa5ff036d9f40f07cb3466a6082e21b8", size = 2494333, upload-time = "2026-04-07T11:14:52.303Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/dd7e1f2aa31a8fbbfc16b0610af1d770ffaf1287490f3c8c5b1c52da264f/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ae6f53f99c9a0eca7a0afc5b4e45fc73bc1dd4ac74c00509031d76df80ed98", size = 4258579, upload-time = "2026-04-07T11:14:54.538Z" }, - { url = "https://files.pythonhosted.org/packages/9c/0a/ac99e1ba347ba0e85e0bb60b74231d55fb93c0eff43f2920ccb413d0be08/rapidfuzz-3.14.5-cp313-cp313-win32.whl", hash = "sha256:4a60f0057231188e3bd30216f7b4e0f279b11fa4ec818bb6c1d9f014d1562fbc", size = 1709231, upload-time = "2026-04-07T11:14:56.524Z" }, - { url = "https://files.pythonhosted.org/packages/cf/cb/0e251d731b3166378644238e8f0cf9e89858c024e19f75ca9f7e3ae83fd5/rapidfuzz-3.14.5-cp313-cp313-win_amd64.whl", hash = "sha256:11bfc2ed8fbe4ab86bd516fadefab126f90e6dcadffa761739fcb304707dfd35", size = 1538519, upload-time = "2026-04-07T11:14:58.635Z" }, - { url = "https://files.pythonhosted.org/packages/30/6f/4548132acc947db6d5346a248e44a8b3a22d608ef30e770fb578caaf2d00/rapidfuzz-3.14.5-cp313-cp313-win_arm64.whl", hash = "sha256:b486b5218808f6f4dc471b114b1054e63553db69705c97da0271f47bd706aedd", size = 812628, upload-time = "2026-04-07T11:15:00.552Z" }, - { url = "https://files.pythonhosted.org/packages/00/60/69b177577290c5eab892c6f75fe89c3aff3f9ae80298a78d9372b1cecb9a/rapidfuzz-3.14.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39ef8658aaf67d51667e7bdaf7096f432333377d8302ac43c70b5df8a4cf89b8", size = 1970231, upload-time = "2026-04-07T11:15:02.603Z" }, - { url = "https://files.pythonhosted.org/packages/48/38/2fd790052659cc4e2907b63c25433f0987864b445c1aeec1a302ef5ad948/rapidfuzz-3.14.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ad37a0be705b544af6296da8edddc260d10a8ae5462530fc9991f66498bb1f9", size = 1194394, upload-time = "2026-04-07T11:15:04.572Z" }, - { url = "https://files.pythonhosted.org/packages/80/f4/28430ad8472fc3536e8ebd51a864a226e979cfe924c6e3f83d111373aa74/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d45e06f60729e07d9b20c205f7e5cff90b6ef2584e852eecf46e045aea69627d", size = 1377051, upload-time = "2026-04-07T11:15:06.728Z" }, - { url = "https://files.pythonhosted.org/packages/77/7e/9aeacabcfd1e77397968362e5b98fe14248b8307011136b17daf99752a8e/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e52da10236aa6212de71b9e170bace65b64b129c0dea7fc243d6c9ce976f5074", size = 3160565, upload-time = "2026-04-07T11:15:08.667Z" }, - { url = "https://files.pythonhosted.org/packages/56/f4/db4dd7be0cd2f2022117ac5407d905f435d60e48baaea313a567ad27e865/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:440d30faaf682ca496170a7f0cc5453ec942e3e079f0fd802c9a7f938dfb50a3", size = 1442113, upload-time = "2026-04-07T11:15:11.138Z" }, - { url = "https://files.pythonhosted.org/packages/a4/99/0e9f6aa57f3e32a767216f797e56dc96b720fcecfb9d8ee907ecc82f8d66/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:56227a61fd3d17b0cd9793132431f3a3d07c8654be96794ba9f89fe0fc8b2d09", size = 2396618, upload-time = "2026-04-07T11:15:13.154Z" }, - { url = "https://files.pythonhosted.org/packages/60/94/44a78e39ffce17cbdd3e2b53b696acc751d5d153be0f499d052b07a4d904/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:2e83cd2e25bb4edd97b689d9979d9c3acccdaaf26ceac08212ceece202febcfa", size = 2478220, upload-time = "2026-04-07T11:15:15.193Z" }, - { url = "https://files.pythonhosted.org/packages/dd/df/454311469a09a507e9d784a35796742bec22e4cebe75551e2da4e0e290fd/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:af3b859726cd3374287e405e14b9634563c078c5531a4f62375508addebddad1", size = 4265027, upload-time = "2026-04-07T11:15:17.28Z" }, - { url = "https://files.pythonhosted.org/packages/fc/01/175465a9ab3e3b70ba669058372f009d1d49c1746e2dcd56b69df188d3a5/rapidfuzz-3.14.5-cp313-cp313t-win32.whl", hash = "sha256:8ce1d850b3c0178440efde9e884d98421b5e87ff925f364d6d79e23910d7593f", size = 1766814, upload-time = "2026-04-07T11:15:19.687Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a0/a9b84a47af06ebed94a1439eb2f02adebfb8628bcd30af1fe3e02f5ef56c/rapidfuzz-3.14.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c84af70bcf34e99aee894e46a0f1ac77f17d0ef828179c387407642e2466d28a", size = 1582448, upload-time = "2026-04-07T11:15:21.98Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f1/5937800238b3f8248e70860d79f69ba8f73e764fff47e36bc9e2f26dbcc6/rapidfuzz-3.14.5-cp313-cp313t-win_arm64.whl", hash = "sha256:aac0ad28c686a5e72b81668b906c030ee28050b244544b8af68e12fb32543895", size = 832932, upload-time = "2026-04-07T11:15:24.358Z" }, - { url = "https://files.pythonhosted.org/packages/81/41/aa3ffb3355e62e1bf91f6599b3092e866bc88487a07c524004943c7676df/rapidfuzz-3.14.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1a31cc6d7d03e7318a0974c038959c59e19c752b81115f2e9138b3331cd64d45", size = 1943327, upload-time = "2026-04-07T11:15:26.266Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e1/c2141f1840a41e07ad2db6f724945f8f8ff3065463899a22939152dd6e09/rapidfuzz-3.14.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0298d357e2bc59d572da4db0bc631009b6f8f6c9bc8c11e99a12b833f16b6575", size = 1161755, upload-time = "2026-04-07T11:15:28.659Z" }, - { url = "https://files.pythonhosted.org/packages/ca/07/66e753eeaa353161d1d331b7dd517bb349b0bacfebe8496d7b26be26f81f/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59b3dba758661a318995655435c6ab20a04ade79fa51e75bc8dc107cac8df280", size = 1376571, upload-time = "2026-04-07T11:15:31.225Z" }, - { url = "https://files.pythonhosted.org/packages/c8/85/9535df0b78ba51f478c9ce7eb6d1f85535cc31fe356773b48fd9d3e563ca/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4900143d82071bdda533b00300c40b14b963ff826b3642cc463b6dd0f036585e", size = 3156468, upload-time = "2026-04-07T11:15:33.428Z" }, - { url = "https://files.pythonhosted.org/packages/81/ee/b667eb93bba6dc4e0de658edd778e1619dc4d6aab68fa5e5c7f075152735/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:feedf219672eef83ea6be6f3bb093bba396a8560fc75be85ba225f082903df0a", size = 1458311, upload-time = "2026-04-07T11:15:35.557Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ce/479074f5624364a48df3403c538797ef22d3ac49c19dc76c3f79fcdcc70c/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419e4397a36e2665ec992d8d64c20ba4b2a42500c76ecadeca78a4f19cb9cc32", size = 2398228, upload-time = "2026-04-07T11:15:37.669Z" }, - { url = "https://files.pythonhosted.org/packages/0b/15/a8982f649150fffbdcd6f17565974501f6ab33b2795267bffbd4a7ba905b/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:97131ab2be39043054ee28d99e09efe316e6d53449b7e962dfcf3c2de8b2b246", size = 2497226, upload-time = "2026-04-07T11:15:39.857Z" }, - { url = "https://files.pythonhosted.org/packages/19/52/5267c03ef6759831b7d4625a0c9c06e87baa2fae084b61ac9c388858317b/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:593c00dac4e30231c35bf3b4f1da8ec0998762e9e94425586a5d636fcd57f9d0", size = 4262283, upload-time = "2026-04-07T11:15:42.279Z" }, - { url = "https://files.pythonhosted.org/packages/71/c0/2579f343a97f5254c43bb5853baccc01488357dcb64a27bcb869b7888a4a/rapidfuzz-3.14.5-cp314-cp314-win32.whl", hash = "sha256:0084b687b02b4e569b46d8d6d4ad25659528e6081cd6d067ca453a69035f07e4", size = 1744614, upload-time = "2026-04-07T11:15:44.498Z" }, - { url = "https://files.pythonhosted.org/packages/17/eb/8edfed1e80119dc9c35b11df4bc701eea85622ad681fff0263b6961d3224/rapidfuzz-3.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:5dfa89d78f22cd773054caff44827b846161a29f2dcf7e78b8f90d086621e502", size = 1588971, upload-time = "2026-04-07T11:15:46.86Z" }, - { url = "https://files.pythonhosted.org/packages/f6/04/5676df93c85cfa57a3045d8047318df9f3cd58c7b8a99340dd95f874795e/rapidfuzz-3.14.5-cp314-cp314-win_arm64.whl", hash = "sha256:67f3f9d2b444268ab53e47d31bab89954888d23c04c6789f2c727e51fe4b1d13", size = 834985, upload-time = "2026-04-07T11:15:49.411Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0d/4a8988cea658fe335048ddef8c876addff1b6daa3c9ca8ad65a5a2196e69/rapidfuzz-3.14.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:77eac0526899b3c3ad1454bb2b03cdb491d67358ec8ef0c9c48bd61b632b431d", size = 1972517, upload-time = "2026-04-07T11:15:51.819Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a3/f5cfd9965a9d9a9e32249159797c47b5d6299ea6d1629f9126b25f1c10a3/rapidfuzz-3.14.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b9c6bd754d11f6e78ac54e3d86b4b11dc1ba2f13e5fc958899574532897f5a99", size = 1196056, upload-time = "2026-04-07T11:15:54.292Z" }, - { url = "https://files.pythonhosted.org/packages/64/07/561c2e40cfd10e6630a7b0ac5a2a813aef50d944bcd1f3d260319d659d5b/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:738c96944d076deeaff70e92b65696ab4f7ecb8081d7791c5403a3257dfaf8ff", size = 1374732, upload-time = "2026-04-07T11:15:56.584Z" }, - { url = "https://files.pythonhosted.org/packages/c2/39/123bb94fee40e2fb3b7c49b80827c7ef42d838e18def3fc2fef5a3cf817a/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4c1bca487a17fe4226b4ffb2d30e799d2b274d692cffa76bd0746f56235fca3", size = 3166902, upload-time = "2026-04-07T11:15:58.768Z" }, - { url = "https://files.pythonhosted.org/packages/75/0a/45716fafc9fd2e028cf20b5ac5bc704887081cd312f84edb0e325599414b/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:af6a90a4ed2a48fa1a2d17e9d824e6c7c950bea5bad0b707c77fd55751e6bfef", size = 1452130, upload-time = "2026-04-07T11:16:01.453Z" }, - { url = "https://files.pythonhosted.org/packages/ca/49/4e96c413114398481c0a5b0086af32c364a18613c9a2ea578d17c4bea4ee/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bf5018938208d4597b2e679a4f8cff9fd252f1df53583130ae56281a21801b64", size = 2396308, upload-time = "2026-04-07T11:16:03.588Z" }, - { url = "https://files.pythonhosted.org/packages/89/b7/49fea9fc6878d59bd259d01dd1972d9b86117992b1c66d9b16f0a65273c3/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c0919d1f89ddf91129906705723118ea09754171e4116f5a5dbc667c7bc9b261", size = 2488210, upload-time = "2026-04-07T11:16:05.871Z" }, - { url = "https://files.pythonhosted.org/packages/0c/44/a1f732b93ffacbdad077b7c801149549b2938e1bece6addb5ad85ed74df8/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:93d8da883a35116d6813432177f35e570db5b0a5e30ecb0cbd7cb39c815735df", size = 4270621, upload-time = "2026-04-07T11:16:08.483Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ce/ff942d19fce5385054650bb71a58495ddda299d94661ccc4e6e7fa44868b/rapidfuzz-3.14.5-cp314-cp314t-win32.whl", hash = "sha256:0f23e37019ec07712d58976b1ab2b889f8649a7f7c2f626a2f34ea9139e79279", size = 1803950, upload-time = "2026-04-07T11:16:10.873Z" }, - { url = "https://files.pythonhosted.org/packages/5c/0f/9aafc63f9661222b819b391c187eed29fc90ad5935f9690e5ecc2d2047a4/rapidfuzz-3.14.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7d5ca9c7832e6879a707296d1463685f7c243a27846227044504741640caec66", size = 1632357, upload-time = "2026-04-07T11:16:13.1Z" }, - { url = "https://files.pythonhosted.org/packages/70/a6/51fc1b0e61e3326e1c68a61cfd0c6b3c34c843681c4b1eefbf0596f59162/rapidfuzz-3.14.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3e91dcd2549b8f8d843f98ba03a17e01f3d8b72ce942adbbb6761bc58ffce813", size = 855409, upload-time = "2026-04-07T11:16:15.787Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ee/e71853bf82846c5c2174b924b71d8e8099fb05ff87c958a720380b434ba3/rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:578e6051f6d5e6200c259b47a103cf06bb875ab5814d17333fc0b5c290b22f4c", size = 1888603, upload-time = "2026-04-07T11:16:18.223Z" }, - { url = "https://files.pythonhosted.org/packages/36/82/40f67b730f32be2ebad9f62add1571c754f52249254b2e88af094b907eee/rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbf1b8bb2695415b347f3727da1addca2acb82c9b97ac86bebf8b1bead1eb12d", size = 1120599, upload-time = "2026-04-07T11:16:20.682Z" }, - { url = "https://files.pythonhosted.org/packages/ef/9f/a3635cc4ec8fc6e14b46e7db1f7f8763d8c4bef33dcc124eea2e6cb2c8f3/rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4a8f5cc84c7ad6bffa0e9947b33eb343ad66e6b53e94fe54378a5508c5ed53", size = 1348524, upload-time = "2026-04-07T11:16:23.451Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1b/2b229520f0b48464cfcd7aa758f74551d12c9bc4ab544022a60210aab064/rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c6d85283629646fa87acc22c66b30ea9d4de7f6fdf887daa2e30fa041829b5", size = 3099302, upload-time = "2026-04-07T11:16:25.858Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b5/363906b1064fc6fe611783a61764927bbd91919aaaabe8cba82151ca93ef/rapidfuzz-3.14.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:dfef96543ced67d9513a422755db422ae1dc34dade0a1485e0b43e7342ed3ebf", size = 1509889, upload-time = "2026-04-07T11:16:28.487Z" }, +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] @@ -1333,6 +1604,280 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, + { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" }, + { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" }, + { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" }, + { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" }, + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" }, + { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" }, + { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" }, +] + [[package]] name = "semver" version = "3.0.4" @@ -1342,6 +1887,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +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 = "simpleeval" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/9d/e7c9309940794dd3073cba2e5101df5874d84243595ce63b1e1c8f9b9c76/simpleeval-1.0.7.tar.gz", hash = "sha256:1e10e5f9fec597814444e20c0892ed15162fa214c8a88f434b5b077cf2fef85b", size = 30250, upload-time = "2026-03-16T10:53:03.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/2f/f32aa85591882378bb43caa09363f3ed97df399369a5144c7f19f2275bc0/simpleeval-1.0.7-py3-none-any.whl", hash = "sha256:97ac271bfd8f2af9e7b9a36ceea67617f26fa873f9d5ae1922f64d4c1442534b", size = 18792, upload-time = "2026-03-16T10:53:02.103Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1351,6 +1914,38 @@ 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 = "tableschema-to-template" +version = "0.0.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema" }, + { name = "pyyaml" }, + { name = "xlsxwriter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/2d/5b5c0127e959a715c5365b3b491846fba5bc1fb7f94370cc3a4fb3f51f68/tableschema-to-template-0.0.13.tar.gz", hash = "sha256:2d8d2250efb840e0ecb9012c5e879a82ef68f65dd86bdff574200fdfc978ff1c", size = 13564, upload-time = "2023-02-01T21:53:17.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/7f/3c129377d9b815c0bb92b8c24bbecabe5e6b13b831f38924f80d2e5b40b2/tableschema_to_template-0.0.13-py3-none-any.whl", hash = "sha256:4905500a4235740654230c3223629d26fdccb7a0457ec1d0110ea102c4f1146c", size = 14860, upload-time = "2023-02-01T21:53:16.02Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + [[package]] name = "tomli" version = "2.4.0" @@ -1430,6 +2025,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/88/25333bbfea6a5dc064371d2002d3d4807db90b84d5448f9106b2712b0fbc/ty-0.0.73-py3-none-win_arm64.whl", hash = "sha256:e47068f8369dea5d641a26a2ad0a947a320b02ff87099b07e95de0323245a4dc", size = 12443573, upload-time = "2026-08-19T03:12:41.449Z" }, ] +[[package]] +name = "typer" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1439,6 +2049,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + [[package]] name = "tzdata" version = "2025.3" @@ -1457,6 +2079,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] +[[package]] +name = "validators" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/66/a435d9ae49850b2f071f7ebd8119dd4e84872b01630d6736761e6e7fd847/validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a", size = 73399, upload-time = "2025-05-01T05:42:06.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712, upload-time = "2025-05-01T05:42:04.203Z" }, +] + [[package]] name = "virtualenv" version = "20.36.1" @@ -1503,3 +2134,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] + +[[package]] +name = "xlrd" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/5a/377161c2d3538d1990d7af382c79f3b2372e880b65de21b01b1a2b78691e/xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9", size = 100167, upload-time = "2025-06-14T08:46:39.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/62/c8d562e7766786ba6587d09c5a8ba9f718ed3fa8af7f4553e8f91c36f302/xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9", size = 96555, upload-time = "2025-06-14T08:46:37.766Z" }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, +] + +[[package]] +name = "xlwt" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/97/56a6f56ce44578a69343449aa5a0d98eefe04085d69da539f3034e2cd5c1/xlwt-1.3.0.tar.gz", hash = "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88", size = 153929, upload-time = "2017-08-22T06:47:16.498Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/48/def306413b25c3d01753603b1a222a011b8621aed27cd7f89cbc27e6b0f4/xlwt-1.3.0-py2.py3-none-any.whl", hash = "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e", size = 99981, upload-time = "2017-08-22T06:47:15.281Z" }, +] From cb8914274360742bb257395edb114a9241b84819 Mon Sep 17 00:00:00 2001 From: TiaTuinstra Date: Fri, 21 Aug 2026 14:01:26 +0000 Subject: [PATCH 2/6] remove internal link from getting_started.md --- docs/user_guide/getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user_guide/getting_started.md b/docs/user_guide/getting_started.md index 5b2380b..cfef590 100644 --- a/docs/user_guide/getting_started.md +++ b/docs/user_guide/getting_started.md @@ -57,7 +57,7 @@ The following columns are **optional** and will be used when present: | `School Id` | | `Unique Id` | -The full schema is defined in [`config/input_schema.yaml`](../../config/input_schema.yaml). If the file is missing any required column, the pipeline will stop immediately with a clear error message listing the missing columns. +The full schema is defined in `config/input_schema.yaml`. If the file is missing any required column, the pipeline will stop immediately with a clear error message listing the missing columns. Place input files in the `input/` subdirectory (not tracked by Git): From 4894d0e66e4066bb5602a6875f4950f019feac8a Mon Sep 17 00:00:00 2001 From: TiaTuinstra Date: Mon, 24 Aug 2026 17:27:48 +0000 Subject: [PATCH 3/6] switch to frictionless json instead of yaml; update test dataset with long client --- config/input_schema.json | 73 ++++++++++++++++++++++++++++++++++++++ input/rodent_dataset.xlsx | Bin 10543 -> 11479 bytes pipeline/preprocess.py | 6 ++-- 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 config/input_schema.json diff --git a/config/input_schema.json b/config/input_schema.json new file mode 100644 index 0000000..db65efa --- /dev/null +++ b/config/input_schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://specs.frictionlessdata.io/schemas/table-schema.json", + "fields": [ + { + "name": "School Type", + "type": "string" + }, + { + "name": "School Name", + "type": "string" + }, + { + "name": "Client Id", + "type": "string", + "constraints": { + "pattern": "^\\d{10}$" + } + }, + { + "name": "First Name", + "type": "string" + }, + { + "name": "Last Name", + "type": "string" + }, + { + "name": "Age", + "type": "integer" + }, + { + "name": "Date of Birth", + "type": "date" + }, + { + "name": "Street Address Line 1", + "type": "string" + }, + { + "name": "Street Address Line 2", + "type": "string" + }, + { + "name": "City", + "type": "string" + }, + { + "name": "Province/Territory", + "type": "string" + }, + { + "name": "Postal Code", + "type": "string" + }, + { + "name": "Overdue Disease", + "type": "string" + }, + { + "name": "Overdue Agent", + "type": "string" + }, + { + "name": "Imms Given", + "type": "string" + }, + { + "name": "Birth Year", + "type": "string" + } + ], + "missingValues": [""] +} diff --git a/input/rodent_dataset.xlsx b/input/rodent_dataset.xlsx index e8effd78f119c6427131dcb6fd8496d744af8cc8..5d8b634b7946b190e6f603dc62025d6a51c12836 100644 GIT binary patch delta 5763 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 delta 4841 zcmY*dXE+-S*G}zSdzIR%V$V{e_Nq~;N>RIrJ&GEs#4II4?I89jRjZ|Djo3wPwQAL_ z84}`q-si`Az2EtDu5+L3{5kVJGuEHXYX`}Qp%SIY5wg2rJKr2B=E(ep8;462gwnv` zsd=NzIDfT8V^o`VC6^z>(ph#vuA-8FwbRni-Yfwz%w~d8ejKUJl)c(DFSu7qYbDdQ zF4CDW@OZ;kf+-?g_9%{>!^qVn79rO4V|b;09P3*KEaBNQROJW2gu7xJE%)%kz~=@d z@XE}XeofA8gbn*niLdC0yAM^L*ZnIg3bH7J<{oj)kH{Eu2{5Z^hPj6_8*jKV&b@l| zZl@NJ`*$B93T`pW-5v;D8`GJJ?}1@-)*%x=F4nV|BP;Npz4KSRTnF$ zmRO)GW1Ql7)sZ1h4%VQ7kp3b;uZ&t^)(&K+UA3bqZdXd83*p2IY^85ds>PvgJ}<^o z2-sP%$+OY#gGn79^bp)ZTfp}LO$Cj$iVo{^cSQw~zx%DYL8M1xU2V*V%!R}+{EBv~ z@<6peHKV)7dCtFVW@|V`cRT_%$R%z6vVHQRz$S^L>}YTL?N!#s`kLT^oI);4cA4Mit=^YJohVLePHaJ!ZX#3W*ebdfzhEK_-;$4)~#moN9zX6 z-_y<)OilVSNTq~}dA@Rvp`FuLRCU+V$4Ml9$cHCiDW+wgFqO-zBHnL(TFf$BDR>=O zKd^E%Ta7ML@<7a9r$2#5W9()g(-f&GdAJ3UxS9Hr&~XIlII)@P1g*315<7FF3lIgm88Xi<LFL_IdWS?$?< zwe_y)D{$3#yzl;(2Vo{Nnl4nXv&G!ODA$Gb(h>sLM%&&d>5G$=3&%Zp#`ug&Ny&nC zXcE5&m3)zXNJBrhf@c=`LxhlCt1VgkpI(BIpn`RlAOkw%3I1nrtuc0v{|30tFBuj! zXMgG6P{Mq|OH-H1Us0(Y1^PPQlc;#RV)?LT<8m-u^M(c|k9B?5VI`^)o!{fpI_#Nd z4O{@*?zdMpXK(SySmJL=$ zfjiiGw{&(@5*0C551#S~XBeZ|YBOj2yjQ+_HvJLpx%hfDzk}!~-7x>daLx~Hk*mjM zw04#O;iGF8N^Er7ron@mtPqu=tyf)DUs)Z&vrodCyKnA$j_HXOaEtcmW2bPj=zEEE z^hSq5!=*US75R z)7(>IpcD_eU})Wiqz9`WIOEZBY(})4tPXEm$A@2&pt!a9%~x-QHvg+DQqnX?>PrCC#BKdD5~k7-iuO zH!BI+-FT6*G_e>+Y$AY4>e$-hkDJd#P}+YxYdSZS-92PhucSf&YpSTAz&Dn$11yS` zvArzUmESktEmVGwm4R0FtQy2w>OjH3gUWZpR>>xEF~GiiWGsExgcuSs!fFhOF5xJK zq|YTllgRldNRtSvuF4mUMxkI_m}dg`yDE=CSfblV+=@N-=5KPS!~?51vY-|Wul7>^ zn06IsujYiKgwKh6@Kc-B_~{mUB?k}mrdb@Tc8ZHT^hqDh_l94x6mDP`8cfX^nT96z zxF_TFXq^e_F2te07tpsNjhKs9_)EQ~wR3U~N8j>pcB6kYjzm+!6=^hGmc{mok)b(Q zsy$+7HP}xZv%r4dNv?r0vb7WrZM+mk)uBcl-GMO;(3-yVr`845oe7!WRNEZBr;lq(_7w3ww#utq~F4fiY^4|~i%%I|5x zZZh`6L=CB>r*Xy!xzeG@E-#RQ@JRj0B{dnj0`VCdP02Shaz`3@8VP$3%S@YEt@3ld z0&J)~={RDtLu4AdNCe0nA9BeiEFsX)+nY3+^Lk{C7CHU3(W$F6n(P0Ky5woQE^eNW z1*g*hmj-u(o?i04KDtQ?XibXGr!fwCUKGSGHn&h$+N$x5mm>{vJiZu|6DpCsUZ-Fa ztI_a|2Kda8zWO__jZPd(l~lqrxMQy4cWQeUKZT&{)S8Dm?!;7nE#NGB1q3aq>JQGNQ&x|1h8FW z>4HC}$yL`|pZ4;{cgwzVqcgx*Rj-T+OvziwXb@uKxkLjkrJ7C0ma$piv`xGd#3-x~ z!hiHZK_A-j0yQJ>pWl$f>@r|ZdW};pi{@rV7HKcc16d3mwG#hJ@T^hEKnn5H^lgKK z27)HJNfm|{br$~svSBk-DaZCU$2`^KMR5hnR(?WI1W?#SvP)jk-ajm%5l{X}uBJmdH3r(N_uzeC?+4=7G!4kN9Bg2_ zFXZZ71?STV;;DJYoQEH0d{d`lKk^B$MLdgMg7xvTXf+NyOmXyKF;wOs3U=~j#878T zpny}Td(*9uTYN?()DLtl_Vp|{18q3Vquub(Yfj6qLlX{SL!&haybTevP4&;s?5 zYheQZzCY&DOpxM$2bkwiVayR1>5WUQp5m&;9K8#=d*!1o)NjmwA{)A=z{J^DN_CE%_aP!nR5Kld$* z;9o)QV8?+k?n7D)tvW|>B^SO|OZrXHuk7nKRTfETEoK(p)1Gv5etp7PnmHrq35z8x zbA%j!@F{y8j=K0Vv%UY_PVWg|Xfgez;>5yk+;cTqy%=%o&O{Y+EOYq2D;F;6yS2H8ZjWQ7aMY|9<5#SDSh>x!y5PTKR~pSqetm6EG}lfgu_cAH1Ed^H)p z{@_z$w>i+tHCtglA1Ro-`{17N|*QZ5g3gRsOn>6DV}X-!I9e+mWp)y~pKW|A^Irt$&i zj#%v^wd=P#YZ566dnm0rjbNTS9*=6{m%|R}PyM3WfWGWR^@c4)!bM-T(KMV^G7QBp9+)Q1zn38s- za#cMRm6OBDm9qC+RX}l5g&LSZ8wBSba939PxJC}fu*^Hp2 z33sUv)$_7Uk7R+lhA-0W#-H8ZHI`tSh(uQpq^Udm@1x~K`ZTwx+ewQmgQ7!C>k|wv zIqTU2+{GIX(I|0lSYoNIp^HKR@To}tS;yx7e{IVKWk8KYCS0xI~^Qg>|^2~?Pa@!87ic3(jO1C^&(_2nwRF|>Q?Ra zj*gvAcSe(4MLUm%tIQq1Lue(9hp9j+#XxQ@{~>>+5%Va(@25iy&w3=cBxY!IB%boG zU2$EU{B=CSJJ3PL9aJ*K_AjZI{#f328}*ZVPP4*slic}wkKkZCYmztprs^^RM^kU< zd%Dv3ox`N8$Ra3UPRt5vU~PM>t!OQdYTi$%GhkTfyPZn8yrIzvULjIPlpS2#k+GF4 zJBiu4`$_LTdF5YW`E@jJOy`$V&#r0M+H6(0N>&YX8>_JHhq;%qhbE<_p&d_^Hy5)1 zx|{x0Q(Asq`+lG8DSnDjJEFJikfUrgRt@xT9TZASRm~}C{bYiT^PB7;J2PQnaoU-p zh(8#jVuNzn-`QZH0>dDv07e_Brmcbue)njHXB{ehok&fHjG;N7vVkr(riM99=yLLd z=Pt{cby(!{?$7!02Z>(FZooVe@`W`|2o~ZsU`oJ}O=0+Q+!C-eY;Q&kU_PdnS-z<-Ljmql_C5*?9 z3T z%vtB``ZPdADJe~R^?mH4vUt+UQK}O)eT9 z!2A431oglotJeg=+)sd-1k}}b|(1ZewhBnQ+T%-4mjCAU+js6H6vx;VB{ z41DCjKSE)=$#aeEz~TMM$b;#Q)cslePGDl9i1EFYQWwouz*Vvp`I9K(|18~l*u96u z|9+81MIrx7 None: ------ ValueError If the file does not conform to the schema defined in - ``config/input_schema.yaml``. + ``config/input_schema.json``. """ - descriptor = yaml.safe_load(INPUT_SCHEMA_PATH.read_text(encoding="utf-8")) + descriptor = json.loads(INPUT_SCHEMA_PATH.read_text(encoding="utf-8")) schema = Schema.from_descriptor(descriptor) report = fl_validate( file_path.name, From 2e5f2493f385df18a9fe04fef8b9188320361da9 Mon Sep 17 00:00:00 2001 From: TiaTuinstra Date: Tue, 25 Aug 2026 16:09:12 +0000 Subject: [PATCH 4/6] Minimizing required columns (deriving where possible); switching to lower_snake_case; adding generated schema .md to docs; support for optional 'version_id column' --- .github/workflows/docs.yml | 5 + .gitignore | 1 + README.md | 20 ++- config/input_schema.json | 32 ++-- config/input_schema.yaml | 37 ---- docs/generate_schema_docs.py | 35 ++++ docs/user_guide/getting_started.md | 7 +- input/rodent_dataset.xlsx | Bin 11479 -> 11149 bytes mkdocs.yml | 1 + pipeline/data_models.py | 11 +- pipeline/preprocess.py | 167 +++++++++---------- pipeline/validate_phix.py | 20 +-- tests/fixtures/sample_input.py | 9 +- tests/integration/test_pipeline_contracts.py | 16 +- tests/unit/test_generate_notices.py | 4 +- tests/unit/test_preprocess.py | 65 ++++---- tests/unit/test_validate_phix.py | 76 ++++----- 17 files changed, 255 insertions(+), 251 deletions(-) delete mode 100644 config/input_schema.yaml create mode 100644 docs/generate_schema_docs.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e1faf67..7435d08 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -21,6 +21,7 @@ on: - '**.png' - '**.svg' - '**.jpeg' + - 'config/input_schema.json' pull_request: branches: - main @@ -38,6 +39,7 @@ on: - '**.png' - '**.svg' - '**.jpeg' + - 'config/input_schema.json' jobs: build: @@ -59,6 +61,9 @@ jobs: - name: Install the project with docs dependencies run: uv sync --group docs + - name: Generate schema docs + run: uv run python docs/generate_schema_docs.py + - name: Build docs run: uv run mkdocs build --strict diff --git a/.gitignore b/.gitignore index 871077e..cfb4e1a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__/ build/ dist/ site/ +docs/user_guide/input_schema.md .coverage htmlcov/ coverage.xml diff --git a/README.md b/README.md index 53f899e..aaa5efe 100644 --- a/README.md +++ b/README.md @@ -282,13 +282,21 @@ The preprocessed artifact contains: "warnings": [], "clients": [ { - "sequence": 1, + "sequence": "00001", "client_id": "1009876545", - "person": {"first_name": "...", "last_name": "...", "date_of_birth": "..."}, - "school": {"name": "...", "board": "..."}, - "contact": {"street_address": "...", "city": "...", "postal_code": "...", "province": "..."}, - "vaccines": {"due": "...", "received": [...]}, - "metadata": {"recipient": "...", "over_16": false} + "language": "en", + "person": { + "first_name": "...", "last_name": "...", + "date_of_birth": "...", "date_of_birth_display": "...", "date_of_birth_iso": "...", + "age": "...", "over_16": true + }, + "school": {"name": "...", "id": "..."}, + "board": {"name": "...", "id": "..."}, + "contact": {"street": "...", "city": "...", "province": "...", "postal_code": "..."}, + "vaccines_due": "...", + "vaccines_due_list": ["..."], + "received": [...], + "metadata": {"version_id": null} }, ... ] diff --git a/config/input_schema.json b/config/input_schema.json index db65efa..6c538d7 100644 --- a/config/input_schema.json +++ b/config/input_schema.json @@ -1,71 +1,75 @@ { - "$schema": "https://specs.frictionlessdata.io/schemas/table-schema.json", + "$schema": "https://datapackage.org/profiles/2.0/tableschema.json", + "title": "input_schema", + "description": "Schema for input file for automated notice generation.", + "fieldsMatch": ["subset"], "fields": [ - { - "name": "School Type", - "type": "string" - }, { "name": "School Name", - "type": "string" + "type": "string", + "description": "School name with PHIX ID (if available) in format 'SCHOOL NAME - PHIX ID'" }, { "name": "Client Id", "type": "string", + "description": "10-digit Panorama client identifier", "constraints": { "pattern": "^\\d{10}$" } }, { "name": "First Name", + "description": "Client first name", "type": "string" }, { "name": "Last Name", + "description": "Client last name", "type": "string" }, - { - "name": "Age", - "type": "integer" - }, { "name": "Date of Birth", + "description": "Client date of birth", "type": "date" }, { "name": "Street Address Line 1", + "description": "Client street address (line 1)", "type": "string" }, { "name": "Street Address Line 2", + "description": "Client street address (line 2)", "type": "string" }, { "name": "City", + "description": "City of client address", "type": "string" }, { "name": "Province/Territory", + "description": "Province / territory of client address", "type": "string" }, { "name": "Postal Code", + "description": "Postal code of client address", "type": "string" }, { "name": "Overdue Disease", + "description": "Comma-separated list of overdue diseases for client", "type": "string" }, { "name": "Overdue Agent", + "description": "Comma-separated list of overdue agents for client", "type": "string" }, { "name": "Imms Given", - "type": "string" - }, - { - "name": "Birth Year", + "description": "List of immunizations given to client, separated by ';'. Each list entry is in the format 'Mon Day, YYYY - '. Optionally, each entry may also include validity status of the dose appended as '- '", "type": "string" } ], diff --git a/config/input_schema.yaml b/config/input_schema.yaml deleted file mode 100644 index 76a18c3..0000000 --- a/config/input_schema.yaml +++ /dev/null @@ -1,37 +0,0 @@ -fields: - - name: School Type - type: string - - name: School Name - type: string - - name: Client Id - type: string - constraints: - pattern: '^\d{10}$' - - name: First Name - type: string - - name: Last Name - type: string - - name: Age - type: integer - - name: Date of Birth - type: date - - name: Street Address Line 1 - type: string - - name: Street Address Line 2 - type: string - - name: City - type: string - - name: Province/Territory - type: string - - name: Postal Code - type: string - - name: Overdue Disease - type: string - - name: Overdue Agent - type: string - - name: Imms Given - type: string - - name: Birth Year - type: string -missingValues: - - '' \ No newline at end of file diff --git a/docs/generate_schema_docs.py b/docs/generate_schema_docs.py new file mode 100644 index 0000000..59ed9a8 --- /dev/null +++ b/docs/generate_schema_docs.py @@ -0,0 +1,35 @@ +from pathlib import Path +import json + +SCHEMA_FILE = Path("config/input_schema.json") +OUTPUT_FILE = Path("docs/user_guide/input_schema.md") + +schema = json.loads(SCHEMA_FILE.read_text()) + +lines = [] + +lines.append(f"# {schema.get('title', 'Schema')}\n\n") + +if schema.get("description"): + lines.append(f"{schema['description']}\n\n") + +lines.append("## Schema Information\n\n") +lines.append("| Property | Value |\n") +lines.append("|----------|-------|\n") +lines.append(f"| Fields | {len(schema['fields'])} |\n") +lines.append(f"| Field Matching | {', '.join(schema.get('fieldsMatch', []))} |\n") + +missing_values = ", ".join(f"`{v}`" for v in schema.get("missingValues", [])) +lines.append(f"| Missing Values | {missing_values} |\n\n") + +lines.append("## Field Summary\n\n") +lines.append("| Field | Type | Description |\n") +lines.append("|-------|------|-------------|\n") + +for field in schema["fields"]: + description = field.get("description", "") + lines.append(f"| {field['name']} | {field['type']} | {description} |\n") + +OUTPUT_FILE.write_text("".join(lines), encoding="utf-8") + +print(f"Generated {OUTPUT_FILE}") diff --git a/docs/user_guide/getting_started.md b/docs/user_guide/getting_started.md index cfef590..ea83eb5 100644 --- a/docs/user_guide/getting_started.md +++ b/docs/user_guide/getting_started.md @@ -31,12 +31,10 @@ The pipeline enforces a strict column schema — column names must match exactly | Column name | Notes | |---|---| -| `School Type` | | | `School Name` | | | `Client Id` | 10-digit numeric string | | `First Name` | | | `Last Name` | | -| `Age` | Integer | | `Date of Birth` | ISO 8601 date (`YYYY-MM-DD`) | | `Street Address Line 1` | | | `Street Address Line 2` | May be blank | @@ -46,7 +44,6 @@ The pipeline enforces a strict column schema — column names must match exactly | `Overdue Disease` | May be blank | | `Overdue Agent` | May be blank | | `Imms Given` | May be blank | -| `Birth Year` | | The following columns are **optional** and will be used when present: @@ -55,9 +52,9 @@ The following columns are **optional** and will be used when present: | `Board Name` | | `Board Id` | | `School Id` | -| `Unique Id` | +| `Version Id` | -The full schema is defined in `config/input_schema.yaml`. If the file is missing any required column, the pipeline will stop immediately with a clear error message listing the missing columns. +The full schema is defined in `config/input_schema.json`. If the file is missing any required column, the pipeline will stop immediately with a clear error message listing the missing columns. Place input files in the `input/` subdirectory (not tracked by Git): diff --git a/input/rodent_dataset.xlsx b/input/rodent_dataset.xlsx index 5d8b634b7946b190e6f603dc62025d6a51c12836..4ad873b5dc9461eaa5b0601d332044b50473c5d4 100644 GIT binary patch delta 5497 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 delta 5817 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 diff --git a/mkdocs.yml b/mkdocs.yml index 54b0ec3..1634708 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -97,6 +97,7 @@ nav: - Getting Started: user_guide/getting_started.md - Configuration Reference: user_guide/configuration.md - PHU Templates: user_guide/phu_templates.md + - Input Schema: user_guide/input_schema.md - PDF Validation: user_guide/pdf_validation.md - Email Package: - Overview: user_guide/email_package/index.md diff --git a/pipeline/data_models.py b/pipeline/data_models.py index a349778..033d8b7 100644 --- a/pipeline/data_models.py +++ b/pipeline/data_models.py @@ -40,20 +40,19 @@ class ClientRecord: person : Dict[str, Any] - Person details: - - full_name: Combined first and last name - - first_name: Given name (optional) - - last_name: Family name (optional) + - first_name: Given name + - last_name: Family name - date_of_birth: Display format (e.g., "Jan 8, 2025") - date_of_birth_iso: ISO format (YYYY-MM-DD) - date_of_birth_display: Localized display format - - age: Calculated age in years + - age: Calculated age in years (as string) - over_16: Boolean flag for age >= 16 school : Dict[str, Any] - School information: name, id, code, type. + School information: name, id. board : Dict[str, Any] - School board information: name, id, code. + School board information: name, id. contact : Dict[str, Any] Contact address: street, city, province, postal_code. diff --git a/pipeline/preprocess.py b/pipeline/preprocess.py index 2d2ae37..a760bfe 100644 --- a/pipeline/preprocess.py +++ b/pipeline/preprocess.py @@ -51,7 +51,6 @@ from datetime import datetime, timezone from hashlib import sha1 from pathlib import Path -from string import Formatter from typing import Any, Dict, List, Literal, Optional import pandas as pd import yaml @@ -73,8 +72,6 @@ LOG = logging.getLogger(__name__) -_FORMATTER = Formatter() - REPLACE_UNSPECIFIED = [ "-unspecified", "unspecified", @@ -86,29 +83,26 @@ INPUT_SCHEMA_PATH = CONFIG_DIR / "input_schema.json" REQUIRED_COLUMN_MAP: dict[str, str] = { - "School Type": "SCHOOL_TYPE", - "School Name": "SCHOOL_NAME", - "Client Id": "CLIENT_ID", - "First Name": "FIRST_NAME", - "Last Name": "LAST_NAME", - "Age": "AGE", - "Date of Birth": "DATE_OF_BIRTH", - "Street Address Line 1": "STREET_ADDRESS_LINE_1", - "Street Address Line 2": "STREET_ADDRESS_LINE_2", - "City": "CITY", - "Province/Territory": "PROVINCE", - "Postal Code": "POSTAL_CODE", - "Overdue Disease": "OVERDUE_DISEASE", - "Overdue Agent": "OVERDUE_AGENT", - "Imms Given": "IMMS_GIVEN", - "Birth Year": "BIRTH_YEAR", + "School Name": "school_name", + "Client Id": "client_id", + "First Name": "first_name", + "Last Name": "last_name", + "Date of Birth": "date_of_birth", + "Street Address Line 1": "street_address_line_1", + "Street Address Line 2": "street_address_line_2", + "City": "city", + "Province/Territory": "province", + "Postal Code": "postal_code", + "Overdue Disease": "overdue_disease", + "Overdue Agent": "overdue_agent", + "Imms Given": "imms_given", } OPTIONAL_COLUMN_MAP: dict[str, str] = { - "Board Name": "BOARD_NAME", - "Board Id": "BOARD_ID", - "School Id": "SCHOOL_ID", - "Unique Id": "UNIQUE_ID", + "Board Name": "board_name", + "Board Id": "board_id", + "School Id": "school_id", + "Version Id": "version_id" } @@ -202,31 +196,31 @@ def check_addresses_complete(df: pd.DataFrame) -> pd.DataFrame: # Normalize text fields: convert to string, strip whitespace, convert "" to NA address_cols = [ - "STREET_ADDRESS_LINE_1", - "STREET_ADDRESS_LINE_2", - "CITY", - "PROVINCE", - "POSTAL_CODE", + "street_address_line_1", + "street_address_line_2", + "city", + "province", + "postal_code", ] for col in address_cols: df[col] = df[col].astype(str).str.strip().replace({"": pd.NA, "nan": pd.NA}) # Build combined address line - df["ADDRESS"] = ( - df["STREET_ADDRESS_LINE_1"].fillna("") + df["address"] = ( + df["street_address_line_1"].fillna("") + " " - + df["STREET_ADDRESS_LINE_2"].fillna("") + + df["street_address_line_2"].fillna("") ).str.strip() - df["ADDRESS"] = df["ADDRESS"].replace({"": pd.NA}) + df["address"] = df["address"].replace({"": pd.NA}) # Check completeness df["address_complete"] = ( - df["ADDRESS"].notna() - & df["CITY"].notna() - & df["PROVINCE"].notna() - & df["POSTAL_CODE"].notna() + df["address"].notna() + & df["city"].notna() + & df["province"].notna() + & df["postal_code"].notna() ) if not df["address_complete"].all(): @@ -266,8 +260,8 @@ def convert_date_iso(date_str: str) -> str: return date_obj.strftime("%Y-%m-%d") -def over_16_check(date_of_birth, date_notice_delivery): - """Check if a client is over 16 years old on notice delivery date. +def calculate_age_at_date(date_of_birth, date_notice_delivery): + """Calculate a client's age on notice delivery date. Parameters ---------- @@ -278,8 +272,8 @@ def over_16_check(date_of_birth, date_notice_delivery): Returns ------- - bool - True if the client is over 16 years old on date_notice_delivery, False otherwise. + int + The client's age on date_notice_delivery. """ birth_datetime = datetime.strptime(date_of_birth, "%Y-%m-%d") @@ -294,7 +288,7 @@ def over_16_check(date_of_birth, date_notice_delivery): ): age -= 1 - return age >= 16 + return age def configure_logging(output_dir: Path, run_id: str) -> Path: @@ -435,7 +429,7 @@ def validate_input(file_path: Path) -> None: def map_columns(df: pd.DataFrame) -> pd.DataFrame: - """Rename input columns to internal UPPER_SNAKE_CASE keys. + """Rename input columns to internal lower_snake_case keys. Required columns are validated for presence; optional columns are renamed only when present. Columns outside both maps are dropped. @@ -525,12 +519,12 @@ def format_vaccine_due_list(vaccine_due_list: list[str]) -> list[str]: def normalize_dataframe(df: pd.DataFrame) -> pd.DataFrame: """Normalize data types on a column-mapped DataFrame. - Expects columns already renamed to UPPER_SNAKE_CASE by map_columns(). + Expects columns already renamed to lower_snake_case by map_columns(). Applies string normalization, date parsing, and numeric coercion. """ working = df.copy() - _skip = {"CLIENT_ID", "DATE_OF_BIRTH", "OVERDUE_DISEASE", "IMMS_GIVEN", "AGE"} + _skip = {"client_id", "date_of_birth", "overdue_disease", "imms_given"} string_required = [v for v in REQUIRED_COLUMN_MAP.values() if v not in _skip] for col in string_required: @@ -542,8 +536,7 @@ def normalize_dataframe(df: pd.DataFrame) -> pd.DataFrame: else: working[col] = working[col].fillna(" ").astype(str).str.strip() - working["DATE_OF_BIRTH"] = pd.to_datetime(working["DATE_OF_BIRTH"], errors="coerce") - working["AGE"] = pd.to_numeric(working["AGE"], errors="coerce") + working["date_of_birth"] = pd.to_datetime(working["date_of_birth"], errors="coerce") return working @@ -605,7 +598,7 @@ def normalize_validity_status(raw_status: Any) -> str: Parameters ---------- raw_status : Any - Raw value extracted from an IMMS_GIVEN segment, or any value that + Raw value extracted from an imms_given segment, or any value that needs to be coerced to a canonical status. Typically a str, but accepts Any so callers need not guard against None or NaN. @@ -663,7 +656,7 @@ def collapse_validity_statuses(statuses: List[Any]) -> str: def classify_dataset_validity( imms_given_series: pd.Series, ) -> Literal["all_present", "all_absent", "mixed"]: - """Scan all IMMS_GIVEN values and classify dataset-level validity coverage. + """Scan all imms_given values and classify dataset-level validity coverage. Performs a pre-pass over the full dataset before the per-client loop so that a single, accurate dataset-level decision can be made about whether @@ -678,7 +671,7 @@ def classify_dataset_validity( Parameters ---------- imms_given_series : pd.Series - The ``IMMS_GIVEN`` column of the normalized working DataFrame. + The ``imms_given`` column of the normalized working DataFrame. Each element is a semicolon-delimited string of dose segments such as ``"May 1, 2020 - DTaP - Valid; Jun 15, 2021 - MMR"``. NaN values and empty strings are silently skipped. @@ -732,7 +725,7 @@ def classify_dataset_validity( def parse_dose_segments( received_agents: Any, replace_unspecified: List[str] ) -> List[Dict[str, str]]: - """Parse an IMMS_GIVEN string into a flat sorted list of individual dose entries. + """Parse an imms_given string into a flat sorted list of individual dose entries. Extracts individual dose entries from a semicolon-delimited string, normalizes dates to ISO format, normalizes validity to one of @@ -744,7 +737,7 @@ def parse_dose_segments( Parameters ---------- received_agents : Any - Raw IMMS_GIVEN cell value. Must be a non-empty ``str`` to be + Raw imms_given cell value. Must be a non-empty ``str`` to be parsed; any other type returns ``[]``. Expected format per segment: ``"MMM D, YYYY - VaccineName"`` or ``"MMM D, YYYY - VaccineName - Valid|Invalid"``. @@ -944,7 +937,7 @@ def build_received_rows( chart_diseases_header: List[str], show_validity_markers: bool = False, ) -> List[Dict[str, Any]]: - """Parse IMMS_GIVEN into display rows with pre-computed per-column validity. + """Parse imms_given into display rows with pre-computed per-column validity. Orchestrates ``parse_dose_segments`` → ``_deduplicate_vaccines_for_date`` → ``_split_into_rows`` for each administration date. Dates whose @@ -956,7 +949,7 @@ def build_received_rows( Parameters ---------- received_agents : Any - Raw IMMS_GIVEN cell value. + Raw imms_given cell value. replace_unspecified : List[str] Vaccine names to suppress. vaccine_reference : Dict[str, Any] @@ -1027,7 +1020,7 @@ def build_preprocess_result( df : pd.DataFrame Raw input DataFrame, typically loaded from an Excel or CSV file. Must have columns already renamed via map_columns() to the internal - UPPER_SNAKE_CASE keys defined in ``REQUIRED_COLUMN_MAP``. + lower_snake_case keys defined in ``REQUIRED_COLUMN_MAP``. language : str Language code for this batch (``"en"`` or ``"fr"``). Stored on every ``ClientRecord`` and used to format display dates. @@ -1036,7 +1029,7 @@ def build_preprocess_result( ``enrich_grouped_records``. replace_unspecified : List[str] Vaccine names to suppress from immunization history. Passed - through to ``process_received_agents``. + through to ``build_received_rows``. config_path : Path, optional Path to ``parameters.yaml``. Defaults to the repository configuration. @@ -1085,22 +1078,22 @@ def build_preprocess_result( include_dose: bool = preprocess_cfg.get("include_dose", False) show_validity_markers: bool = preprocess_cfg.get("show_validity_markers", False) - working["SCHOOL_ID"] = working.apply( + working["school_id"] = working.apply( lambda row: synthesize_identifier( - row.get("SCHOOL_ID", ""), row["SCHOOL_NAME"], "sch" + row.get("school_id", ""), row["school_name"], "sch" ), axis=1, ) - working["BOARD_ID"] = working.apply( + working["board_id"] = working.apply( lambda row: synthesize_identifier( - row.get("BOARD_ID", ""), row.get("BOARD_NAME", ""), "brd" + row.get("board_id", ""), row.get("board_name", ""), "brd" ), axis=1, ) - if (working["BOARD_NAME"] == "").any(): + if (working["board_name"] == "").any(): affected = ( - working.loc[working["BOARD_NAME"] == "", "SCHOOL_NAME"].unique().tolist() + working.loc[working["board_name"] == "", "school_name"].unique().tolist() ) warnings.add( "Missing board name for: " + ", ".join(sorted(filter(None, affected))) @@ -1109,12 +1102,12 @@ def build_preprocess_result( ) sorted_df = working.sort_values( - by=["SCHOOL_NAME", "LAST_NAME", "FIRST_NAME", "CLIENT_ID"], + by=["school_name", "last_name", "first_name", "client_id"], kind="stable", ).reset_index(drop=True) - sorted_df["SEQUENCE"] = [f"{idx + 1:05d}" for idx in range(len(sorted_df))] + sorted_df["sequence"] = [f"{idx + 1:05d}" for idx in range(len(sorted_df))] - validity_coverage = classify_dataset_validity(sorted_df["IMMS_GIVEN"]) + validity_coverage = classify_dataset_validity(sorted_df["imms_given"]) if validity_coverage == "mixed": if show_validity_markers: raise ValueError( @@ -1134,11 +1127,11 @@ def build_preprocess_result( clients: List[ClientRecord] = [] for row in sorted_df.itertuples(index=False): - client_id = str(row.CLIENT_ID) # type: ignore[attr-defined] - sequence = row.SEQUENCE # type: ignore[attr-defined] + client_id = str(row.client_id) # type: ignore[attr-defined] + sequence = row.sequence # type: ignore[attr-defined] dob_iso = ( - row.DATE_OF_BIRTH.strftime("%Y-%m-%d") # type: ignore[attr-defined] - if pd.notna(row.DATE_OF_BIRTH) # type: ignore[attr-defined] + row.date_of_birth.strftime("%Y-%m-%d") # type: ignore[attr-defined] + if pd.notna(row.date_of_birth) # type: ignore[attr-defined] else None ) if dob_iso is None: @@ -1150,7 +1143,7 @@ def build_preprocess_result( if language_enum == Language.FRENCH and dob_iso else (convert_date_string(dob_iso, locale="en") if dob_iso else None) ) - vaccines_due = process_vaccines_due(row.OVERDUE_DISEASE, language) # type: ignore[attr-defined] + vaccines_due = process_vaccines_due(row.overdue_disease, language) # type: ignore[attr-defined] vaccines_due_list = [ item.strip() for item in vaccines_due.split(",") if item.strip() ] @@ -1166,48 +1159,48 @@ def build_preprocess_result( else: vaccines_due_list = hide_vaccine_due_doses(vaccines_due_list) received = build_received_rows( - row.IMMS_GIVEN, # type: ignore[attr-defined] + row.imms_given, # type: ignore[attr-defined] replace_unspecified, vaccine_reference, chart_diseases_header, show_validity_markers, ) - postal_code = row.POSTAL_CODE if row.POSTAL_CODE else "Not provided" # type: ignore[attr-defined] + postal_code = row.postal_code if row.postal_code else "Not provided" # type: ignore[attr-defined] address_line = " ".join( - filter(None, [row.STREET_ADDRESS_LINE_1, row.STREET_ADDRESS_LINE_2]) # type: ignore[attr-defined] + filter(None, [row.street_address_line_1, row.street_address_line_2]) # type: ignore[attr-defined] ).strip() - if not pd.isna(row.AGE): # type: ignore[attr-defined] - over_16 = bool(row.AGE >= 16) # type: ignore[attr-defined] - elif dob_iso and date_notice_delivery: - over_16 = over_16_check(dob_iso, date_notice_delivery) + if dob_iso and date_notice_delivery: + age = calculate_age_at_date(dob_iso, date_notice_delivery) + over_16 = age >= 16 else: + age = None over_16 = False person = { - "first_name": row.FIRST_NAME or "", # type: ignore[attr-defined] - "last_name": row.LAST_NAME or "", # type: ignore[attr-defined] + "first_name": row.first_name or "", # type: ignore[attr-defined] + "last_name": row.last_name or "", # type: ignore[attr-defined] "date_of_birth": dob_iso or "", "date_of_birth_display": formatted_dob or "", "date_of_birth_iso": dob_iso or "", - "age": str(row.AGE) if not pd.isna(row.AGE) else "", # type: ignore[attr-defined] + "age": str(age) or "", # type: ignore[attr-defined] "over_16": over_16, } school = { - "name": row.SCHOOL_NAME, # type: ignore[attr-defined] - "id": row.SCHOOL_ID, # type: ignore[attr-defined] + "name": row.school_name, # type: ignore[attr-defined] + "id": row.school_id, # type: ignore[attr-defined] } board = { - "name": row.BOARD_NAME or "", # type: ignore[attr-defined] - "id": row.BOARD_ID, # type: ignore[attr-defined] + "name": row.board_name or "", # type: ignore[attr-defined] + "id": row.board_id, # type: ignore[attr-defined] } contact = { "street": address_line, - "city": row.CITY, # type: ignore[attr-defined] - "province": row.PROVINCE, # type: ignore[attr-defined] + "city": row.city, # type: ignore[attr-defined] + "province": row.province, # type: ignore[attr-defined] "postal_code": postal_code, } @@ -1223,7 +1216,7 @@ def build_preprocess_result( vaccines_due_list=vaccines_due_list if vaccines_due_list else None, received=received if received else None, metadata={ - "unique_id": row.UNIQUE_ID or None, # type: ignore[attr-defined] + "version_id": row.version_id or None, # type: ignore[attr-defined] }, ) @@ -1300,7 +1293,7 @@ def run_phix_validation( target_phu=target_phu, output_dir=output_dir, unmatched_behavior=phix_config.get("unmatched_behavior", "warn"), - column_prefix=phix_config.get("column_prefix", "PHIX_"), + column_prefix=phix_config.get("column_prefix", "phix_"), ) diff --git a/pipeline/validate_phix.py b/pipeline/validate_phix.py index afc1b1e..6d27282 100644 --- a/pipeline/validate_phix.py +++ b/pipeline/validate_phix.py @@ -251,18 +251,18 @@ def validate_schools( mapping_path: Path, target_phu: str, output_dir: Path, - school_column: str = "SCHOOL_NAME", + school_column: str = "school_name", unmatched_behavior: str = "warn", - column_prefix: str = "PHIX_", + column_prefix: str = "phix_", ) -> tuple[pd.DataFrame, list[str]]: """Validate school names in *df* against the PHIX mapping for *target_phu*. Adds four columns to the DataFrame (with *column_prefix*): - - ``{prefix}FACILITY_ID`` : Matched facility ID, or ``""`` - - ``{prefix}MATCH_TYPE`` : ``"exact"`` | ``"inexact"`` | ``"no_match"`` - - ``{prefix}MATCHED_NAME`` : Canonical name from mapping, or ``""`` - - ``{prefix}MATCHED_PHU`` : *target_phu* for matched rows, ``""`` otherwise + - ``{prefix}facility_id`` : Matched facility ID, or ``""`` + - ``{prefix}match_type`` : ``"exact"`` | ``"inexact"`` | ``"no_match"`` + - ``{prefix}matched_name`` : Canonical name from mapping, or ``""`` + - ``{prefix}matched_phu`` : *target_phu* for matched rows, ``""`` otherwise @@ -328,16 +328,16 @@ def _attr(raw_val: object, attr: str, default: str = "") -> str: def col(suffix: str) -> str: return f"{column_prefix}{suffix}" - df[col("FACILITY_ID")] = df[school_column].apply( + df[col("facility_id")] = df[school_column].apply( lambda x: _attr(x, "matched_id") if pd.notna(x) else "" ) - df[col("MATCH_TYPE")] = df[school_column].apply( + df[col("match_type")] = df[school_column].apply( lambda x: _attr(x, "match_type", "no_match") if pd.notna(x) else "no_match" ) - df[col("MATCHED_NAME")] = df[school_column].apply( + df[col("matched_name")] = df[school_column].apply( lambda x: _attr(x, "matched_name") if pd.notna(x) else "" ) - df[col("MATCHED_PHU")] = df[school_column].apply( + df[col("matched_phu")] = df[school_column].apply( lambda x: target_phu if pd.notna(x) and _attr(x, "match_type", "no_match") != "no_match" else "" ) diff --git a/tests/fixtures/sample_input.py b/tests/fixtures/sample_input.py index 712e163..560e437 100644 --- a/tests/fixtures/sample_input.py +++ b/tests/fixtures/sample_input.py @@ -49,7 +49,6 @@ def create_test_input_dataframe( DataFrame with columns matching expected Excel input format """ data: Dict[str, List[Any]] = { - "School Type": ["Public", "Public", "Catholic", "Catholic", "Public"][:num_clients], "School Name": [ "Tunnel Academy", "Cheese Wheel Academy", @@ -60,7 +59,6 @@ def create_test_input_dataframe( "Client Id": [f"{i:010d}" for i in range(1, num_clients + 1)], "First Name": ["Alice", "Benoit", "Chloe", "Diana", "Ethan"][:num_clients], "Last Name": ["Zephyr", "Arnaud", "Brown", "Davis", "Evans"][:num_clients], - "Age": ["10", "11", "12", "10", "11"][:num_clients], "Date of Birth": [ "2015-01-02", "2014-05-06", @@ -113,7 +111,6 @@ def create_test_input_dataframe( if include_immunization_history else [""] * num_clients ), - "Birth Year": ["2015", "2014", "2013", "2015", "2014"][:num_clients], } return pd.DataFrame(data) @@ -188,14 +185,12 @@ def create_test_client_record( school_dict: Dict[str, Any] = { "id": f"sch_{sequence}", - "name": school_name, - "code": "SCH001", + "name": school_name } board_dict: Dict[str, Any] = { "id": f"brd_{sequence}", - "name": board_name, - "code": "BRD001", + "name": board_name } received: List[Dict[str, object]] = [] diff --git a/tests/integration/test_pipeline_contracts.py b/tests/integration/test_pipeline_contracts.py index f3bc7e3..865c3a7 100644 --- a/tests/integration/test_pipeline_contracts.py +++ b/tests/integration/test_pipeline_contracts.py @@ -253,7 +253,7 @@ def test_disease_alias_normalized_to_canonical_name( Assertion: vaccines_due_list contains "Polio", not the raw alias "Poliomyelitis" """ df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) - df["OVERDUE_DISEASE"] = ["Poliomyelitis"] + df["overdue_disease"] = ["Poliomyelitis"] result = preprocess.build_preprocess_result( df, @@ -301,7 +301,7 @@ def test_unknown_validity_warns_when_markers_enabled( monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) - df["IMMS_GIVEN"] = ["May 1, 2020 - DTaP"] + df["imms_given"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( df, @@ -353,7 +353,7 @@ def test_mixed_validity_with_markers_enabled_raises_value_error( df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) # First client has a suffixed dose; second has an un-suffixed dose → mixed - df["IMMS_GIVEN"] = [ + df["imms_given"] = [ "May 1, 2020 - DTaP - Valid", "Jun 15, 2021 - MMR", ] @@ -398,7 +398,7 @@ def test_mixed_validity_with_markers_disabled_warns_and_succeeds( monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) - df["IMMS_GIVEN"] = [ + df["imms_given"] = [ "May 1, 2020 - DTaP - Valid", "Jun 15, 2021 - MMR", ] @@ -449,7 +449,7 @@ def test_include_dose_formats_vaccines_due_list( monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) - df["OVERDUE_DISEASE"] = ["DTaP - 2"] + df["overdue_disease"] = ["DTaP - 2"] result = preprocess.build_preprocess_result( df, @@ -480,7 +480,7 @@ def test_include_dose_requires_dose_bearing_schema( encoding="utf-8", ) df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) - df["OVERDUE_DISEASE"] = ["Polio"] + df["overdue_disease"] = ["Polio"] with pytest.raises(ValueError, match="include_dose requires overdue entries"): preprocess.build_preprocess_result( @@ -508,7 +508,7 @@ def test_blank_dose_warns_and_displays_only_disease( encoding="utf-8", ) df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) - df["OVERDUE_DISEASE"] = ["Polio - "] + df["overdue_disease"] = ["Polio - "] result = preprocess.build_preprocess_result( df, @@ -688,7 +688,7 @@ def test_phix_toggle_via_run_phix_validation( normalized_test_df, tmp_path ) - has_phix = "PHIX_MATCH_TYPE" in result_df.columns + has_phix = "phix_match_type" in result_df.columns assert has_phix == expect_phix_cols, ( f"Expected PHIX columns={expect_phix_cols}, got {has_phix}" ) diff --git a/tests/unit/test_generate_notices.py b/tests/unit/test_generate_notices.py index 7c454a9..55683b5 100644 --- a/tests/unit/test_generate_notices.py +++ b/tests/unit/test_generate_notices.py @@ -55,8 +55,8 @@ def test_read_artifact_with_valid_json(self, tmp_test_dir: Path) -> None: "date_of_birth_display": "Jan 01, 2015", "date_of_birth_iso": "2015-01-01", }, - "school": {"name": "Test School", "code": "SCH001"}, - "board": {"name": "Test Board", "code": "BRD001"}, + "school": {"name": "Test School"}, + "board": {"name": "Test Board"}, "contact": { "street": "123 Main St", "city": "Toronto", diff --git a/tests/unit/test_preprocess.py b/tests/unit/test_preprocess.py index cc3fae4..7fa262c 100644 --- a/tests/unit/test_preprocess.py +++ b/tests/unit/test_preprocess.py @@ -65,7 +65,7 @@ def test_renames_required_columns_to_internal_keys(self): def test_province_territory_maps_to_province(self): df = _make_conforming_df() result = preprocess.map_columns(df) - assert "PROVINCE" in result.columns + assert "province" in result.columns assert "Province/Territory" not in result.columns def test_drops_unknown_columns(self): @@ -73,18 +73,18 @@ def test_drops_unknown_columns(self): df["Extra Column"] = ["surprise"] result = preprocess.map_columns(df) assert "Extra Column" not in result.columns - assert "EXTRA_COLUMN" not in result.columns + assert "extra_column" not in result.columns def test_includes_optional_column_when_present(self): df = _make_conforming_df() df["Board Name"] = ["District Board"] result = preprocess.map_columns(df) - assert "BOARD_NAME" in result.columns + assert "board_name" in result.columns def test_omits_optional_column_when_absent(self): df = _make_conforming_df() result = preprocess.map_columns(df) - assert "BOARD_NAME" not in result.columns + assert "board_name" not in result.columns def test_raises_on_missing_required_column(self): df = _make_conforming_df() @@ -221,33 +221,33 @@ def test_normalize_dataframe_passes_valid_dataframe(self) -> None: def test_normalize_dataframe_handles_missing_values(self) -> None: """Verify NaN/None values in string columns are filled.""" df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) - df.loc[0, "STREET_ADDRESS_LINE_2"] = None - df.loc[1, "POSTAL_CODE"] = float("nan") + df.loc[0, "street_address_line_2"] = None + df.loc[1, "postal_code"] = float("nan") result = preprocess.normalize_dataframe(df) - assert result["STREET_ADDRESS_LINE_2"].iloc[0] == "" - assert result["POSTAL_CODE"].iloc[1] == "" + assert result["street_address_line_2"].iloc[0] == "" + assert result["postal_code"].iloc[1] == "" def test_normalize_dataframe_converts_dates(self) -> None: - """Verify DATE_OF_BIRTH is parsed to datetime.""" + """Verify date_of_birth is parsed to datetime.""" df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) - df["DATE_OF_BIRTH"] = ["2015-01-02", "2014-05-06"] + df["date_of_birth"] = ["2015-01-02", "2014-05-06"] result = preprocess.normalize_dataframe(df) - assert pd.api.types.is_datetime64_any_dtype(result["DATE_OF_BIRTH"]) + assert pd.api.types.is_datetime64_any_dtype(result["date_of_birth"]) def test_normalize_dataframe_trims_whitespace(self) -> None: """Verify string columns have leading/trailing whitespace stripped.""" df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) - df["FIRST_NAME"] = [" Alice "] - df["LAST_NAME"] = [" Zephyr "] + df["first_name"] = [" Alice "] + df["last_name"] = [" Zephyr "] result = preprocess.normalize_dataframe(df) - assert result["FIRST_NAME"].iloc[0] == "Alice" - assert result["LAST_NAME"].iloc[0] == "Zephyr" + assert result["first_name"].iloc[0] == "Alice" + assert result["last_name"].iloc[0] == "Zephyr" @pytest.mark.unit @@ -261,7 +261,8 @@ def test_over_16_check_true_for_over_16(self) -> None: - Notices sent to student (not parent) if over 16 - Must correctly classify students by age """ - result = preprocess.over_16_check("2000-01-01", "2020-05-15") + age = preprocess.calculate_age_at_date("2000-01-01", "2020-05-15") + result = age >= 16 assert result is True @@ -271,7 +272,8 @@ def test_over_16_check_false_for_under_16(self) -> None: Real-world significance: - Notices sent to parent for students under 16 """ - result = preprocess.over_16_check("2010-01-01", "2020-05-15") + age = preprocess.calculate_age_at_date("2010-01-01", "2020-05-15") + result = age >= 16 assert result is False @@ -281,7 +283,8 @@ def test_over_16_check_boundary_at_16(self) -> None: Real-world significance: - Must correctly handle 16th birthday (inclusive) """ - result = preprocess.over_16_check("2000-05-15", "2016-05-15") + age = preprocess.calculate_age_at_date("2000-05-15", "2016-05-15") + result = age >= 16 assert result is True @@ -489,7 +492,7 @@ def test_build_result_maps_vaccines_correctly( - Affects disease coverage reporting in notices """ df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) - df["IMMS_GIVEN"] = ["May 1, 2020 - DTaP"] + df["imms_given"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( df, @@ -529,8 +532,8 @@ def test_build_result_uses_explicit_config_path( encoding="utf-8", ) df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) - df["OVERDUE_DISEASE"] = ["DTaP - 2"] - df["IMMS_GIVEN"] = ["May 1, 2020 - DTaP"] + df["overdue_disease"] = ["DTaP - 2"] + df["imms_given"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( df, @@ -641,8 +644,8 @@ def test_build_result_detects_duplicate_client_ids( """ df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) # Force duplicate client IDs - df.loc[0, "CLIENT_ID"] = "C123456789" - df.loc[1, "CLIENT_ID"] = "C123456789" + df.loc[0, "client_id"] = "C123456789" + df.loc[1, "client_id"] = "C123456789" result = preprocess.build_preprocess_result( df, @@ -672,11 +675,11 @@ def test_build_result_detects_multiple_duplicate_client_ids( """ df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=5)) # Create two sets of duplicates - df.loc[0, "CLIENT_ID"] = "C111111111" - df.loc[1, "CLIENT_ID"] = "C111111111" - df.loc[2, "CLIENT_ID"] = "C111111111" - df.loc[3, "CLIENT_ID"] = "C222222222" - df.loc[4, "CLIENT_ID"] = "C222222222" + df.loc[0, "client_id"] = "C111111111" + df.loc[1, "client_id"] = "C111111111" + df.loc[2, "client_id"] = "C111111111" + df.loc[3, "client_id"] = "C222222222" + df.loc[4, "client_id"] = "C222222222" result = preprocess.build_preprocess_result( df, @@ -954,7 +957,7 @@ def test_nan_and_empty_strings_are_ignored(self) -> None: """Verify NaN values and blank cells do not count as absent-suffix doses. Real-world significance: - - Clients with no immunization history have empty IMMS_GIVEN cells; + - Clients with no immunization history have empty imms_given cells; these must not trigger "mixed" when the rest of the dataset is clean Assertion: valid suffixed dose + NaN + "" → "all_present" @@ -992,7 +995,7 @@ class TestParseDoseSegments: - replace_unspecified filtering Real-world significance: - - This function is the sole parser for IMMS_GIVEN strings; incorrect + - This function is the sole parser for imms_given strings; incorrect parsing silently omits or misrepresents a patient's immunization history on their printed notice. """ @@ -1291,7 +1294,7 @@ def test_build_result_maps_vaccines_correctly(self, default_vaccine_reference) - - DTaP → Diphtheria, Tetanus, Pertussis columns populated. """ df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) - df["IMMS_GIVEN"] = ["May 1, 2020 - DTaP"] + df["imms_given"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( df, diff --git a/tests/unit/test_validate_phix.py b/tests/unit/test_validate_phix.py index 3803cf2..6cf8b44 100644 --- a/tests/unit/test_validate_phix.py +++ b/tests/unit/test_validate_phix.py @@ -69,7 +69,7 @@ def phix_config_yaml(tmp_path: Path, mapping_file: Path) -> Path: "mapping_file": str(mapping_file), "target_phu": "Test PHU", "unmatched_behavior": "warn", - "column_prefix": "PHIX_", + "column_prefix": "phix_", } } p = tmp_path / "parameters.yaml" @@ -360,27 +360,27 @@ class TestValidateSchools: @pytest.fixture def base_df(self) -> pd.DataFrame: - """Minimal DataFrame with a SCHOOL_NAME column for validation tests.""" + """Minimal DataFrame with a school_name column for validation tests.""" return pd.DataFrame({ - "SCHOOL_NAME": [ + "school_name": [ "Springfield Elementary - 001", # exact "Shelbyville Middle", # inexact/name_only (no ID) "Unknown Academy", # no_match ], - "CLIENT_ID": ["C1", "C2", "C3"], + "client_id": ["C1", "C2", "C3"], }) def test_adds_phix_columns_to_dataframe(self, base_df, mapping_file, tmp_path): - """validate_schools adds four PHIX_ columns to the returned DataFrame. + """validate_schools adds four phix_ columns to the returned DataFrame. Real-world significance: - - Downstream audit steps depend on PHIX_MATCH_TYPE, PHIX_FACILITY_ID, - PHIX_MATCHED_NAME, and PHIX_MATCHED_PHU being present. + - Downstream audit steps depend on phix_match_type, phix_facility_id, + phix_matched_name, and phix_matched_phu being present. Assertion: all four columns present in result DataFrame. """ result_df, _ = validate_phix.validate_schools(base_df, mapping_file, "Test PHU", tmp_path) - for col in ["PHIX_FACILITY_ID", "PHIX_MATCH_TYPE", "PHIX_MATCHED_NAME", "PHIX_MATCHED_PHU"]: + for col in ["phix_facility_id", "phix_match_type", "phix_matched_name", "phix_matched_phu"]: assert col in result_df.columns def test_exact_match_row_has_correct_values(self, base_df, mapping_file, tmp_path): @@ -389,19 +389,19 @@ def test_exact_match_row_has_correct_values(self, base_df, mapping_file, tmp_pat Assertion: PHIX columns correct for the exact-match row. """ result_df, _ = validate_phix.validate_schools(base_df, mapping_file, "Test PHU", tmp_path) - exact_row = result_df[result_df["CLIENT_ID"] == "C1"].iloc[0] - assert exact_row["PHIX_MATCH_TYPE"] == "exact" - assert exact_row["PHIX_FACILITY_ID"] == "001" - assert exact_row["PHIX_MATCHED_PHU"] == "Test PHU" + exact_row = result_df[result_df["client_id"] == "C1"].iloc[0] + assert exact_row["phix_match_type"] == "exact" + assert exact_row["phix_facility_id"] == "001" + assert exact_row["phix_matched_phu"] == "Test PHU" def test_no_match_row_has_empty_phu(self, base_df, mapping_file, tmp_path): - """No-match row has empty PHIX_MATCHED_PHU. + """No-match row has empty phix_matched_phu. - Assertion: PHIX_MATCHED_PHU is empty string for no_match rows. + Assertion: phix_matched_phu is empty string for no_match rows. """ result_df, _ = validate_phix.validate_schools(base_df, mapping_file, "Test PHU", tmp_path) - no_match_row = result_df[result_df["CLIENT_ID"] == "C3"].iloc[0] - assert no_match_row["PHIX_MATCHED_PHU"] == "" + no_match_row = result_df[result_df["client_id"] == "C3"].iloc[0] + assert no_match_row["phix_matched_phu"] == "" def test_warn_behavior_returns_all_rows_and_warning(self, base_df, mapping_file, tmp_path): """unmatched_behavior='warn' keeps all rows and returns a warning string. @@ -444,21 +444,21 @@ def test_skip_behavior_filters_unmatched_rows(self, mapping_file, tmp_path): """ # Use raw values with and without ID suffix to exercise both code paths df = pd.DataFrame({ - "SCHOOL_NAME": [ + "school_name": [ "Springfield Elementary - 001", # exact (raw value has ' - ID' suffix) "Shelbyville Middle", # inexact/name_only "Unknown Academy", # no_match ], - "CLIENT_ID": ["C1", "C2", "C3"], + "client_id": ["C1", "C2", "C3"], }) result_df, _ = validate_phix.validate_schools( df, mapping_file, "Test PHU", tmp_path, unmatched_behavior="skip" ) - assert set(result_df["CLIENT_ID"]) == {"C1", "C2"} - assert "C3" not in result_df["CLIENT_ID"].values + assert set(result_df["client_id"]) == {"C1", "C2"} + assert "C3" not in result_df["client_id"].values def test_missing_school_column_returns_df_unchanged(self, mapping_file, tmp_path): - """DataFrame without SCHOOL_NAME column passes through unchanged. + """DataFrame without school_name column passes through unchanged. Real-world significance: - Prevents hard crashes when the input is missing the expected column; @@ -472,32 +472,32 @@ def test_missing_school_column_returns_df_unchanged(self, mapping_file, tmp_path assert warnings == [] def test_nan_values_in_school_column_treated_as_no_match(self, mapping_file, tmp_path): - """NaN in SCHOOL_NAME column does not crash; those rows get no_match columns. + """NaN in school_name column does not crash; those rows get no_match columns. Real-world significance: - Sparse input files often have blank rows in the school column. - Assertion: NaN rows have PHIX_MATCH_TYPE='no_match' and empty PHU. + Assertion: NaN rows have phix_match_type='no_match' and empty PHU. """ df = pd.DataFrame({ - "SCHOOL_NAME": ["Springfield Elementary - 001", None], - "CLIENT_ID": ["C1", "C2"], + "school_name": ["Springfield Elementary - 001", None], + "client_id": ["C1", "C2"], }) result_df, _ = validate_phix.validate_schools(df, mapping_file, "Test PHU", tmp_path) - nan_row = result_df[result_df["CLIENT_ID"] == "C2"].iloc[0] - assert nan_row["PHIX_MATCH_TYPE"] == "no_match" - assert nan_row["PHIX_MATCHED_PHU"] == "" + nan_row = result_df[result_df["client_id"] == "C2"].iloc[0] + assert nan_row["phix_match_type"] == "no_match" + assert nan_row["phix_matched_phu"] == "" def test_custom_column_prefix_applied(self, base_df, mapping_file, tmp_path): """column_prefix parameter changes output column names. - Assertion: columns use the supplied prefix instead of 'PHIX_'. + Assertion: columns use the supplied prefix instead of 'phix_'. """ result_df, _ = validate_phix.validate_schools( - base_df, mapping_file, "Test PHU", tmp_path, column_prefix="VAL_" + base_df, mapping_file, "Test PHU", tmp_path, column_prefix="val_" ) - assert "VAL_MATCH_TYPE" in result_df.columns - assert "PHIX_MATCH_TYPE" not in result_df.columns + assert "val_match_type" in result_df.columns + assert "phix_match_type" not in result_df.columns def test_writes_three_csv_audit_files(self, base_df, mapping_file, tmp_path): """CSV audit files are written to output_dir for non-empty categories. @@ -521,7 +521,7 @@ def test_does_not_mutate_input_dataframe(self, base_df, mapping_file, tmp_path): - Callers must be able to compare original and enriched DataFrames; in-place mutation would break that and could cause subtle bugs. - Assertion: original DataFrame has no PHIX_ columns after the call. + Assertion: original DataFrame has no phix_ columns after the call. """ original_cols = set(base_df.columns) validate_phix.validate_schools(base_df, mapping_file, "Test PHU", tmp_path) @@ -545,8 +545,8 @@ class TestRunPhixValidation: def base_df(self) -> pd.DataFrame: """Minimal DataFrame for preprocess.run_phix_validation tests.""" return pd.DataFrame({ - "SCHOOL_NAME": ["Springfield Elementary - 001", "Unknown Academy"], - "CLIENT_ID": ["C1", "C2"], + "school_name": ["Springfield Elementary - 001", "Unknown Academy"], + "client_id": ["C1", "C2"], }) def test_disabled_returns_df_unchanged_and_no_warnings(self, tmp_path, base_df): @@ -619,13 +619,13 @@ def test_enabled_with_valid_config_enriches_df( - The happy path: Step 2 enriches client data with PHIX validation metadata used by public-health staff for audit. - Assertion: PHIX_ columns present; warnings returned for unmatched school. + Assertion: phix_ columns present; warnings returned for unmatched school. """ with patch.object(preprocess, "PARAMETERS_PATH", phix_config_yaml): result_df, warnings = preprocess.run_phix_validation(base_df, tmp_path) - assert "PHIX_MATCH_TYPE" in result_df.columns - assert "PHIX_FACILITY_ID" in result_df.columns + assert "phix_match_type" in result_df.columns + assert "phix_facility_id" in result_df.columns # "Unknown Academy" has no match → warning issued assert len(warnings) > 0 From f5189e05ba64c735b55102887b9b5541f8d639aa Mon Sep 17 00:00:00 2001 From: TiaTuinstra Date: Thu, 27 Aug 2026 16:01:38 +0000 Subject: [PATCH 5/6] change schema to use lower_snake_case; change test input to excel file with lower_snake_case --- .gitignore | 2 +- CHANGELOG.md | 10 ++ config/input_schema.json | 108 +++++++++--- docs/user_guide/getting_started.md | 34 ++-- input/rodent_dataset.csv | 8 + input/rodent_dataset.xlsx | Bin 11149 -> 0 bytes pipeline/orchestrator.py | 2 +- pipeline/preprocess.py | 86 +++------- pyproject.toml | 2 +- tests/fixtures/sample_input.py | 28 +-- tests/integration/test_pipeline_contracts.py | 20 +-- tests/unit/test_orchestrator.py | 4 - tests/unit/test_preprocess.py | 170 +++++++------------ uv.lock | 2 +- 14 files changed, 225 insertions(+), 251 deletions(-) create mode 100644 input/rodent_dataset.csv delete mode 100644 input/rodent_dataset.xlsx diff --git a/.gitignore b/.gitignore index cfb4e1a..fa329ee 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ docs/user_guide/input_schema.md htmlcov/ coverage.xml coverage.json -!input/rodent_dataset.xlsx +!input/rodent_dataset.csv input/* phu_templates/* !phu_templates/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 30063c7..72e72bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [v1.1.0](https://github.com/WDGPH/ImmuKnow/releases/tag/v1.1.0) - 2026-08-25 + +[Compare with v1.0.0](https://github.com/WDGPH/ImmuKnow/compare/v1.0.0...v1.1.0) + +### Changed + +- Switch input schema from fuzzy matching / data normalization to enforced Frictionless schema validation. Add schema page to mkdocs. +- Minimize required columns by deriving where possible. +- Adopt `lower_snake_case` column naming. + ## [v1.0.0](https://github.com/WDGPH/ImmuKnow/releases/tag/v1.0.0) - 2026-08-13 [Compare with v0.3.0](https://github.com/WDGPH/ImmuKnow/compare/v0.3.0...v1.0.0) diff --git a/config/input_schema.json b/config/input_schema.json index 6c538d7..c660b73 100644 --- a/config/input_schema.json +++ b/config/input_schema.json @@ -5,72 +5,132 @@ "fieldsMatch": ["subset"], "fields": [ { - "name": "School Name", + "name": "school_name", "type": "string", - "description": "School name with PHIX ID (if available) in format 'SCHOOL NAME - PHIX ID'" + "description": "School name with PHIX ID (if available) in format 'SCHOOL NAME - PHIX ID'", + "stripWhitespace": true, + "constraints": { + "required": true + } }, { - "name": "Client Id", + "name": "client_id", "type": "string", "description": "10-digit Panorama client identifier", + "stripWhitespace": true, "constraints": { + "required": true, "pattern": "^\\d{10}$" } }, { - "name": "First Name", + "name": "first_name", "description": "Client first name", - "type": "string" + "type": "string", + "stripWhitespace": true, + "constraints": { + "required": true + } }, { - "name": "Last Name", + "name": "last_name", "description": "Client last name", - "type": "string" + "type": "string", + "stripWhitespace": true, + "constraints": { + "required": true + } }, { - "name": "Date of Birth", + "name": "date_of_birth", "description": "Client date of birth", - "type": "date" + "type": "date", + "constraints": { + "required": true + } }, { - "name": "Street Address Line 1", + "name": "street_address_line_1", "description": "Client street address (line 1)", - "type": "string" + "type": "string", + "stripWhitespace": true }, { - "name": "Street Address Line 2", + "name": "street_address_line_2", "description": "Client street address (line 2)", - "type": "string" + "type": "string", + "stripWhitespace": true }, { - "name": "City", + "name": "city", "description": "City of client address", - "type": "string" + "type": "string", + "stripWhitespace": true, + "constraints": { + "required": true + } }, { - "name": "Province/Territory", + "name": "province", "description": "Province / territory of client address", - "type": "string" + "type": "string", + "stripWhitespace": true }, { - "name": "Postal Code", + "name": "postal_code", "description": "Postal code of client address", - "type": "string" + "type": "string", + "stripWhitespace": true }, { - "name": "Overdue Disease", + "name": "overdue_disease", "description": "Comma-separated list of overdue diseases for client", - "type": "string" + "type": "string", + "stripWhitespace": true, + "constraints": { + "required": true + } }, { - "name": "Overdue Agent", + "name": "overdue_agent", "description": "Comma-separated list of overdue agents for client", - "type": "string" + "type": "string", + "stripWhitespace": true, + "constraints": { + "required": true + } }, { - "name": "Imms Given", + "name": "imms_given", "description": "List of immunizations given to client, separated by ';'. Each list entry is in the format 'Mon Day, YYYY - '. Optionally, each entry may also include validity status of the dose appended as '- '", + "type": "string", + "stripWhitespace": true, + "constraints": { + "required": true + } + }, + { + "name": "board_name", + "description": "School board name", + "type": "string", + "stripWhitespace": true + }, + { + "name": "board_id", + "description": "School board identifier", + "type": "string", + "stripWhitespace": true + }, + { + "name": "school_id", + "description": "School identifier", "type": "string" + }, + { + "name": "version_id", + "description": "Version identifier", + "type": "string", + "stripWhitespace": true } ], "missingValues": [""] diff --git a/docs/user_guide/getting_started.md b/docs/user_guide/getting_started.md index ea83eb5..ae4f79b 100644 --- a/docs/user_guide/getting_started.md +++ b/docs/user_guide/getting_started.md @@ -31,28 +31,28 @@ The pipeline enforces a strict column schema — column names must match exactly | Column name | Notes | |---|---| -| `School Name` | | -| `Client Id` | 10-digit numeric string | -| `First Name` | | -| `Last Name` | | -| `Date of Birth` | ISO 8601 date (`YYYY-MM-DD`) | -| `Street Address Line 1` | | -| `Street Address Line 2` | May be blank | -| `City` | | -| `Province/Territory` | | -| `Postal Code` | | -| `Overdue Disease` | May be blank | -| `Overdue Agent` | May be blank | -| `Imms Given` | May be blank | +| `school_name` | | +| `client_id` | 10-digit numeric string | +| `first_name` | | +| `last_name` | | +| `date_of_birth` | ISO 8601 date (`YYYY-MM-DD`) | +| `street_address_line_1` | | +| `street_address_line_2` | May be blank | +| `city` | | +| `province` | | +| `postal_code` | | +| `overdue_disease` | May be blank | +| `overdue_agent` | May be blank | +| `imms_given` | May be blank | The following columns are **optional** and will be used when present: | Column name | |---| -| `Board Name` | -| `Board Id` | -| `School Id` | -| `Version Id` | +| `board_name` | +| `board_id` | +| `school_id` | +| `version_id` | The full schema is defined in `config/input_schema.json`. If the file is missing any required column, the pipeline will stop immediately with a clear error message listing the missing columns. diff --git a/input/rodent_dataset.csv b/input/rodent_dataset.csv new file mode 100644 index 0000000..0d1e725 --- /dev/null +++ b/input/rodent_dataset.csv @@ -0,0 +1,8 @@ +school_name,client_id,first_name,last_name,date_of_birth,street_address_line_1,street_address_line_2,city,province,postal_code,overdue_disease,overdue_agent,imms_given,Disease(s)/Agent(s),Imms History by Agent +WHISKER ELEMENTARY-1009876543,1009876543,Squeak,McCheese,2013-06-15,14 Burrow Lane,,Cheddarville,Ontario,M1C3E5,"Varicella, HPV, Hepatitis B","Var, HPV-9, Men-C-ACYW-135,","Aug 20, 2013 - DTaP-IPV-Hib; Aug 20, 2013 - Pneu-C-13; Aug 20, 2013 - rota-unspecified; Nov 18, 2013 - DTaP-IPV-Hib; Nov 18, 2013 - Pneu-C-13; Jan 25, 2014 - DTaP-IPV-Hib; May 12, 2014 - MMR; May 12, 2014 - Men-C-C; Oct 3, 2014 - Var; Apr 14, 2024 - Tdap-IPV;",Varicella (Var),"[2013 AUG 20: DTaP-IPV-Hib, Pneu-C-13, rota-unspecified] [2013 NOV 18: DTaP-IPV-Hib, Pneu-C-13] [2014 JAN 25: DTaP-IPV-Hib] [2014 MAY 12: MMR, Men-C-C] [2014 OCT 03: Var] [2024 APR 14: Tdap-IPV, MMR-Var]" +CHEESE WHEEL ACADEMY-1009876544,1009876544,Nibble,Sharpcheddar,2014-04-22,22 Gouda St,,Fromage City,Ontario,C3H3Z9,"Measles,","MMR,","Jul 10, 2014 - DTaP-IPV-Hib; Jul 10, 2014 - Pneu-C-13; Sep 15, 2014 - DTaP-IPV-Hib; Nov 20, 2014 - rota-unspecified; Mar 2, 2015 - MMR; Mar 2, 2015 - Men-C-C; Aug 7, 2015 - Var; Oct 1, 2015 - DTaP-IPV-Hib; May 19, 2024 - Tdap-IPV;",Measles (MMR),"[2014 JUL 10: DTaP-IPV-Hib, Pneu-C-13] [2014 SEP 15: DTaP-IPV-Hib] [2014 NOV 20: rota-unspecified] [2015 MAR 02: MMR, Men-C-C] [2015 AUG 07: Var] [2015 OCT 01: DTaP-IPV-Hib] [2024 MAY 19: Tdap-IPV]" +BURROW PUBLIC SCHOOL-1009876545,1009876545,Scurry,Nutcracker,2012-11-30,7 Tunnel Road,Unit 2,Gnawtown,Ontario,G9N8R2,"Hepatitis B,","HB,","Jan 5, 2013 - DTaP-IPV-Hib; Jan 5, 2013 - rota-unspecified; Mar 7, 2013 - Pneu-C-13; May 9, 2013 - DTaP-IPV-Hib; Jun 11, 2013 - MMR; Oct 23, 2013 - Men-C-C; Feb 2, 2014 - Var; May 6, 2014 - Pneu-C-13; Sep 12, 2014 - DTaP-IPV-Hib; May 1, 2024 - Tdap-IPV;",Hepatitis B (HB),"[2013 JAN 05: DTaP-IPV-Hib, rota-unspecified] [2013 MAR 07: Pneu-C-13] [2013 MAY 09: DTaP-IPV-Hib] [2013 JUN 11: MMR] [2013 OCT 23: Men-C-C] [2014 FEB 02: Var] [2014 MAY 06: Pneu-C-13] [2014 SEP 12: DTaP-IPV-Hib] [2024 MAY 01: Tdap-IPV]" +TUNNEL ACADEMY-1009876546,1009876546,Whiskers,Greyfur,2013-09-10,88 Haystack Drive,,Burrowville,Ontario,H8Y6T5,"Mumps,","MMR,","Oct 15, 2013 - DTaP-IPV-Hib; Dec 12, 2013 - rota-unspecified; Jan 17, 2014 - Pneu-C-13; Apr 8, 2014 - DTaP-IPV-Hib; Jun 19, 2014 - MMR; Oct 22, 2014 - Men-C-C; Feb 4, 2015 - Var; Sep 9, 2015 - DTaP-IPV-Hib; Apr 10, 2024 - Tdap-IPV;",Mumps (MMR),[2013 OCT 15: DTaP-IPV-Hib] [2013 DEC 12: rota-unspecified] [2014 JAN 17: Pneu-C-13] [2014 APR 08: DTaP-IPV-Hib] [2014 JUN 19: MMR] [2014 OCT 22: Men-C-C] [2015 FEB 04: Var] [2015 SEP 09: DTaP-IPV-Hib] [2024 APR 10: Tdap-IPV] +NUTCRACKER ACADEMY-1009876547,1009876547,Chisel,Teetherson,2014-02-28,3 Acorn Ave,Suite 1,Hazelton,Ontario,N4U2L1,"HPV,","HPV-9,","Mar 12, 2014 - DTaP-IPV-Hib; Mar 12, 2014 - rota-unspecified; May 14, 2014 - Pneu-C-13; Jul 19, 2014 - DTaP-IPV-Hib; Sep 21, 2014 - MMR; Nov 25, 2014 - Men-C-C; Apr 17, 2015 - Var; Sep 13, 2015 - DTaP-IPV-Hib; May 5, 2024 - Tdap-IPV;",HPV (HPV-9),"[2014 MAR 12: DTaP-IPV-Hib, rota-unspecified] [2014 MAY 14: Pneu-C-13] [2014 JUL 19: DTaP-IPV-Hib] [2014 SEP 21: MMR] [2014 NOV 25: Men-C-C] [2015 APR 17: Var] [2015 SEP 13: DTaP-IPV-Hib] [2024 MAY 05: Tdap-IPV]" +NUTCRACKER ACADEMY-1009876547,1009876548,Ratty,Teetherson,2009-02-28,,,Hazelton,Ontario,N4U2L1,"HPV,","HPV-9,","Mar 12, 2014 - DTaP-IPV-Hib; Mar 12, 2014 - rota-unspecified; May 14, 2014 - Pneu-C-13; Jul 19, 2014 - DTaP-IPV-Hib; Sep 21, 2014 - MMR; Nov 25, 2014 - Men-C-C; Apr 17, 2015 - Var; Sep 13, 2015 - DTaP-IPV-Hib; May 5, 2024 - Tdap-IPV;",HPV (HPV-9),"[2014 MAR 12: DTaP-IPV-Hib, rota-unspecified] [2014 MAY 14: Pneu-C-13] [2014 JUL 19: DTaP-IPV-Hib] [2014 SEP 21: MMR] [2014 NOV 25: Men-C-C] [2015 APR 17: Var] [2015 SEP 13: DTaP-IPV-Hib] [2024 MAY 05: Tdap-IPV]" +TUNNEL ACADEMY-1009876550,1009876550,Cheddarina,Swiftpaws,2014-09-14,44 Hayloft Road,,Burrowville,Ontario,H8Y6T6,MMR,MMR,"Jan 10, 2015 - DTaP-IPV-Hib; Jan 29, 2015 - Pneu-C-13; Feb 18, 2015 - rota-unspecified; Mar 07, 2015 - DTaP-IPV-Hib; Mar 28, 2015 - MMR; Apr 15, 2015 - Men-C-C; May 02, 2015 - Var; May 27, 2015 - DTaP-IPV-Hib; Jun 16, 2015 - Pneu-C-13; Jul 09, 2015 - Influenza (IIV4); Aug 01, 2015 - Influenza (IIV4); Aug 29, 2015 - MMR; Sep 22, 2015 - Var; Oct 11, 2015 - DTaP-IPV-Hib; Nov 05, 2015 - Pneu-C-13; Dec 03, 2015 - Men-C-C; Jan 14, 2016 - MMR; Feb 06, 2016 - Influenza (IIV4); Mar 12, 2016 - Hep A; Apr 04, 2016 - Hep A booster; May 18, 2016 - Yellow Fever; Jun 07, 2016 - Rabies (pre-exposure); Jun 30, 2016 - Rabies (pre-exposure) dose 2; Jul 23, 2016 - Rabies (pre-exposure) dose 3; Aug 15, 2016 - Var; Sep 08, 2016 - DTaP-IPV-Hib; Oct 01, 2016 - Pneu-C-13; Oct 27, 2016 - Influenza (IIV4); Nov 19, 2016 - MMR; Dec 14, 2016 - Men-C-C; Jan 09, 2017 - Var; Feb 03, 2017 - DTaP-IPV-Hib; Mar 01, 2017 - Pneu-C-13; Mar 29, 2017 - MMR; Apr 18, 2017 - Influenza (IIV4); May 10, 2017 - COVID-19 (Pfizer Pediatric); Jun 02, 2017 - COVID-19 (Pfizer Pediatric) dose 2; Jun 28, 2017 - COVID-19 Booster; Jul 20, 2017 - Var; Aug 12, 2017 - Men-C-C; Sep 03, 2017 - Influenza (IIV4); Oct 25, 2017 - DTaP-IPV-Hib; Nov 16, 2017 - Pneu-C-13; Dec 08, 2017 - MMR; May 02, 2023 - Tdap; Jan 18, 2024 - Men-C-ACYW-135; May 01, 2024 - Tdap-IPV",Measles (MMR),[2015 JAN 10: DTaP-IPV-Hib] [2015 JAN 29: Pneu-C-13] [2015 FEB 18: rota-unspecified] [2015 MAR 07: DTaP-IPV-Hib] [2015 MAR 28: MMR] [2015 APR 15: Men-C-C] [2015 MAY 02: Var] [2015 MAY 27: DTaP-IPV-Hib] [2015 JUN 16: Pneu-C-13] [2015 JUL 09: Influenza (IIV4)] [2015 AUG 01: Influenza (IIV4)] [2015 AUG 29: MMR] [2015 SEP 22: Var] [2015 OCT 11: DTaP-IPV-Hib] [2015 NOV 05: Pneu-C-13] [2015 DEC 03: Men-C-C] [2016 JAN 14: MMR] [2016 FEB 06: Influenza (IIV4)] [2016 MAR 12: Hep A] [2016 APR 04: Hep A booster] [2016 MAY 18: Yellow Fever] [2016 JUN 07: Rabies (pre-exposure)] [2016 JUN 30: Rabies (pre-exposure) dose 2] [2016 JUL 23: Rabies (pre-exposure) dose 3] [2016 AUG 15: Var] [2016 SEP 08: DTaP-IPV-Hib] [2016 OCT 01: Pneu-C-13] [2016 OCT 27: Influenza (IIV4)] [2016 NOV 19: MMR] [2016 DEC 14: Men-C-C] [2017 JAN 09: Var] [2017 FEB 03: DTaP-IPV-Hib] [2017 MAR 01: Pneu-C-13] [2017 MAR 29: MMR] [2017 APR 18: Influenza (IIV4)] [2017 MAY 10: COVID-19 (Pfizer Pediatric)] [2017 JUN 02: COVID-19 (Pfizer Pediatric) dose 2] [2017 JUN 28: COVID-19 Booster] [2017 JUL 20: Var] [2017 AUG 12: Men-C-C] [2017 SEP 03: Influenza (IIV4)] [2017 OCT 25: DTaP-IPV-Hib] [2017 NOV 16: Pneu-C-13] [2017 DEC 08: MMR] [2023 MAY 02: Tdap] [2024 JAN 18: Men-C-ACYW-135] [2024 MAY 01: Tdap-IPV] diff --git a/input/rodent_dataset.xlsx b/input/rodent_dataset.xlsx deleted file mode 100644 index 4ad873b5dc9461eaa5b0601d332044b50473c5d4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11149 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 pd.DataFrame: try: if ext in [".xlsx", ".xls"]: - df = pd.read_excel(file_path, engine="openpyxl", dtype={"Client Id": str}) + df = pd.read_excel(file_path, engine="openpyxl", dtype={"client_id": str}) elif ext == ".csv": # Try common encodings for enc in ["utf-8-sig", "latin-1", "cp1252"]: @@ -428,39 +405,6 @@ def validate_input(file_path: Path) -> None: ) -def map_columns(df: pd.DataFrame) -> pd.DataFrame: - """Rename input columns to internal lower_snake_case keys. - - Required columns are validated for presence; optional columns are renamed - only when present. Columns outside both maps are dropped. - - Parameters - ---------- - df : pd.DataFrame - Raw input DataFrame with source column names as loaded from the input file. - - Returns - ------- - pd.DataFrame - DataFrame with columns renamed to the internal keys defined in - ``REQUIRED_COLUMN_MAP`` and ``OPTIONAL_COLUMN_MAP``. Unrecognised - columns are dropped. - - Raises - ------ - ValueError - If any required column is missing from the DataFrame. - """ - missing = [col for col in REQUIRED_COLUMN_MAP if col not in df.columns] - if missing: - raise ValueError(f"Input is missing required columns: {missing}") - - present_optional = {k: v for k, v in OPTIONAL_COLUMN_MAP.items() if k in df.columns} - full_map = {**REQUIRED_COLUMN_MAP, **present_optional} - renamed = df.rename(columns=full_map) - known = set(full_map.values()) - return renamed[[col for col in renamed.columns if col in known]] - def split_vaccine_due_entry(item: str) -> tuple[str, str | None]: """Separate a disease name from its optional dose field.""" @@ -516,21 +460,32 @@ def format_vaccine_due_list(vaccine_due_list: list[str]) -> list[str]: return formatted +_REQUIRED_STRING_COLS = [ + "school_name", + "first_name", + "last_name", + "street_address_line_1", + "street_address_line_2", + "city", + "province", + "postal_code", + "overdue_agent", +] + +_OPTIONAL_COLS = ["board_name", "board_id", "school_id", "version_id"] + + def normalize_dataframe(df: pd.DataFrame) -> pd.DataFrame: - """Normalize data types on a column-mapped DataFrame. + """Normalize data types on a DataFrame with snake_case column names. - Expects columns already renamed to lower_snake_case by map_columns(). Applies string normalization, date parsing, and numeric coercion. """ working = df.copy() - _skip = {"client_id", "date_of_birth", "overdue_disease", "imms_given"} - string_required = [v for v in REQUIRED_COLUMN_MAP.values() if v not in _skip] - - for col in string_required: + for col in _REQUIRED_STRING_COLS: working[col] = working[col].fillna(" ").astype(str).str.strip() - for col in OPTIONAL_COLUMN_MAP.values(): + for col in _OPTIONAL_COLS: if col not in working.columns: working[col] = "" else: @@ -1019,8 +974,7 @@ def build_preprocess_result( ---------- df : pd.DataFrame Raw input DataFrame, typically loaded from an Excel or CSV file. - Must have columns already renamed via map_columns() to the internal - lower_snake_case keys defined in ``REQUIRED_COLUMN_MAP``. + Must have lower_snake_case column names matching the input schema. language : str Language code for this batch (``"en"`` or ``"fr"``). Stored on every ``ClientRecord`` and used to format display dates. diff --git a/pyproject.toml b/pyproject.toml index 8209e16..6f0cb65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ packages = ["pipeline", "templates"] [project] name = "ImmuKnow" -version = "1.0.0" +version = "1.1.0" requires-python = ">=3.10" dependencies = [ "pandas", diff --git a/tests/fixtures/sample_input.py b/tests/fixtures/sample_input.py index 560e437..84c88e8 100644 --- a/tests/fixtures/sample_input.py +++ b/tests/fixtures/sample_input.py @@ -49,42 +49,42 @@ def create_test_input_dataframe( DataFrame with columns matching expected Excel input format """ data: Dict[str, List[Any]] = { - "School Name": [ + "school_name": [ "Tunnel Academy", "Cheese Wheel Academy", "Mountain Heights Public School", "River Valley Elementary", "Downtown Collegiate", ][:num_clients], - "Client Id": [f"{i:010d}" for i in range(1, num_clients + 1)], - "First Name": ["Alice", "Benoit", "Chloe", "Diana", "Ethan"][:num_clients], - "Last Name": ["Zephyr", "Arnaud", "Brown", "Davis", "Evans"][:num_clients], - "Date of Birth": [ + "client_id": [f"{i:010d}" for i in range(1, num_clients + 1)], + "first_name": ["Alice", "Benoit", "Chloe", "Diana", "Ethan"][:num_clients], + "last_name": ["Zephyr", "Arnaud", "Brown", "Davis", "Evans"][:num_clients], + "date_of_birth": [ "2015-01-02", "2014-05-06", "2013-08-15", "2015-03-22", "2014-11-10", ][:num_clients], - "Board Name": [ + "board_name": [ "Guelph Board of Education", "Guelph Board of Education", "Wellington Board of Education", "Wellington Board of Education", "Ontario Public Schools", ][:num_clients], - "Street Address Line 1": [ + "street_address_line_1": [ "123 Main St", "456 Side Rd", "789 Oak Ave", "321 Elm St", "654 Maple Dr", ][:num_clients], - "Street Address Line 2": ["", "Suite 5", "", "Apt 12", ""][:num_clients], - "City": ["Guelph", "Guelph", "Wellington", "Wellington", "Toronto"][:num_clients], - "Province/Territory": ["ON", "ON", "ON", "ON", "ON"][:num_clients], - "Postal Code": ["N1H 2T2", "N1H 2T3", "N1K 1B2", "N1K 1B3", "M5V 3A8"][:num_clients], - "Overdue Disease": ( + "street_address_line_2": ["", "Suite 5", "", "Apt 12", ""][:num_clients], + "city": ["Guelph", "Guelph", "Wellington", "Wellington", "Toronto"][:num_clients], + "province": ["ON", "ON", "ON", "ON", "ON"][:num_clients], + "postal_code": ["N1H 2T2", "N1H 2T3", "N1K 1B2", "N1K 1B3", "M5V 3A8"][:num_clients], + "overdue_disease": ( [ "Measles/Mumps/Rubella", "Haemophilus influenzae infection, invasive", @@ -95,12 +95,12 @@ def create_test_input_dataframe( if include_overdue else [""] * num_clients ), - "Overdue Agent": ( + "overdue_agent": ( ["MMR", "Hib", "DTaP", "IPV", "PCV13"][:num_clients] if include_overdue else [""] * num_clients ), - "Imms Given": ( + "imms_given": ( [ "May 01, 2020 - DTaP; Jun 15, 2021 - MMR", "Apr 10, 2019 - IPV", diff --git a/tests/integration/test_pipeline_contracts.py b/tests/integration/test_pipeline_contracts.py index 865c3a7..2b39aa6 100644 --- a/tests/integration/test_pipeline_contracts.py +++ b/tests/integration/test_pipeline_contracts.py @@ -252,7 +252,7 @@ def test_disease_alias_normalized_to_canonical_name( Assertion: vaccines_due_list contains "Polio", not the raw alias "Poliomyelitis" """ - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df = sample_input.create_test_input_dataframe(num_clients=1) df["overdue_disease"] = ["Poliomyelitis"] result = preprocess.build_preprocess_result( @@ -300,7 +300,7 @@ def test_unknown_validity_warns_when_markers_enabled( ) monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df = sample_input.create_test_input_dataframe(num_clients=1) df["imms_given"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( @@ -351,7 +351,7 @@ def test_mixed_validity_with_markers_enabled_raises_value_error( ) monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) + df = sample_input.create_test_input_dataframe(num_clients=2) # First client has a suffixed dose; second has an un-suffixed dose → mixed df["imms_given"] = [ "May 1, 2020 - DTaP - Valid", @@ -397,7 +397,7 @@ def test_mixed_validity_with_markers_disabled_warns_and_succeeds( ) monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) + df = sample_input.create_test_input_dataframe(num_clients=2) df["imms_given"] = [ "May 1, 2020 - DTaP - Valid", "Jun 15, 2021 - MMR", @@ -448,7 +448,7 @@ def test_include_dose_formats_vaccines_due_list( ) monkeypatch.setattr(preprocess, "PARAMETERS_PATH", params_path) - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df = sample_input.create_test_input_dataframe(num_clients=1) df["overdue_disease"] = ["DTaP - 2"] result = preprocess.build_preprocess_result( @@ -479,7 +479,7 @@ def test_include_dose_requires_dose_bearing_schema( "preprocess:\n include_dose: true\n", encoding="utf-8", ) - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df = sample_input.create_test_input_dataframe(num_clients=1) df["overdue_disease"] = ["Polio"] with pytest.raises(ValueError, match="include_dose requires overdue entries"): @@ -507,7 +507,7 @@ def test_blank_dose_warns_and_displays_only_disease( "preprocess:\n include_dose: true\n", encoding="utf-8", ) - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df = sample_input.create_test_input_dataframe(num_clients=1) df["overdue_disease"] = ["Polio - "] result = preprocess.build_preprocess_result( @@ -552,13 +552,13 @@ def phix_mapping_file(tmp_path: Path) -> Path: @pytest.fixture def normalized_test_df() -> pd.DataFrame: - """Return a normalized DataFrame (post map_columns + normalize_dataframe). + """Return a normalized DataFrame (post normalize_dataframe). This is the shape of DataFrame that run_phix_validation receives in the - real pipeline — after column mapping and normalization, before artifact build. + real pipeline — after normalization, before artifact build. """ raw = sample_input.create_test_input_dataframe(num_clients=3) - return preprocess.normalize_dataframe(preprocess.map_columns(raw)) + return preprocess.normalize_dataframe(raw) @pytest.mark.integration diff --git a/tests/unit/test_orchestrator.py b/tests/unit/test_orchestrator.py index 7d13a6e..e4bdf21 100644 --- a/tests/unit/test_orchestrator.py +++ b/tests/unit/test_orchestrator.py @@ -229,10 +229,6 @@ def test_run_step_2_passes_selected_config_path(self, tmp_path: Path) -> None: return_value=MagicMock(), ), patch("pipeline.orchestrator.preprocess.validate_input"), - patch( - "pipeline.orchestrator.preprocess.map_columns", - return_value=MagicMock(), - ), patch( "pipeline.orchestrator.preprocess.normalize_dataframe", return_value=MagicMock(), diff --git a/tests/unit/test_preprocess.py b/tests/unit/test_preprocess.py index 7fa262c..aa00597 100644 --- a/tests/unit/test_preprocess.py +++ b/tests/unit/test_preprocess.py @@ -32,72 +32,24 @@ def _make_conforming_df(**overrides) -> pd.DataFrame: """Build a minimal DataFrame with all required input columns.""" row = { - "School Type": ["Public"], - "School Name": ["Test School"], - "Client Id": ["C001"], - "First Name": ["Alice"], - "Last Name": ["Zephyr"], - "Age": ["10"], - "Date of Birth": ["2015-01-01"], - "Street Address Line 1": ["123 Main St"], - "Street Address Line 2": [""], - "City": ["Guelph"], - "Province/Territory": ["ON"], - "Postal Code": ["N1H 2T2"], - "Overdue Disease": ["Measles"], - "Overdue Agent": ["MMR"], - "Imms Given": [""], - "Birth Year": ["2015"], + "school_name": ["Test School"], + "client_id": ["C001"], + "first_name": ["Alice"], + "last_name": ["Zephyr"], + "date_of_birth": ["2015-01-01"], + "street_address_line_1": ["123 Main St"], + "street_address_line_2": [""], + "city": ["Guelph"], + "province": ["ON"], + "postal_code": ["N1H 2T2"], + "overdue_disease": ["Measles"], + "overdue_agent": ["MMR"], + "imms_given": [""], } row.update(overrides) return pd.DataFrame(row) -@pytest.mark.unit -class TestMapColumns: - """Unit tests for map_columns() strict column mapping.""" - - def test_renames_required_columns_to_internal_keys(self): - df = _make_conforming_df() - result = preprocess.map_columns(df) - assert set(preprocess.REQUIRED_COLUMN_MAP.values()).issubset(set(result.columns)) - - def test_province_territory_maps_to_province(self): - df = _make_conforming_df() - result = preprocess.map_columns(df) - assert "province" in result.columns - assert "Province/Territory" not in result.columns - - def test_drops_unknown_columns(self): - df = _make_conforming_df() - df["Extra Column"] = ["surprise"] - result = preprocess.map_columns(df) - assert "Extra Column" not in result.columns - assert "extra_column" not in result.columns - - def test_includes_optional_column_when_present(self): - df = _make_conforming_df() - df["Board Name"] = ["District Board"] - result = preprocess.map_columns(df) - assert "board_name" in result.columns - - def test_omits_optional_column_when_absent(self): - df = _make_conforming_df() - result = preprocess.map_columns(df) - assert "board_name" not in result.columns - - def test_raises_on_missing_required_column(self): - df = _make_conforming_df() - df = df.drop(columns=["School Name"]) - with pytest.raises(ValueError, match="missing required columns"): - preprocess.map_columns(df) - - def test_raises_listing_all_missing_columns(self): - df = _make_conforming_df() - df = df.drop(columns=["School Name", "First Name"]) - with pytest.raises(ValueError, match="missing required columns"): - preprocess.map_columns(df) - @pytest.mark.unit class TestFormatVaccineDueList: @@ -177,7 +129,7 @@ def test_read_input_xlsx_file(self, tmp_test_dir: Path) -> None: df_read = preprocess.read_input(input_path) assert len(df_read) == 3 - assert "School Name" in df_read.columns + assert "school_name" in df_read.columns def test_read_input_missing_file_raises_error(self, tmp_test_dir: Path) -> None: """Verify error when input file doesn't exist. @@ -211,7 +163,7 @@ class TestNormalizeDataFrame: def test_normalize_dataframe_passes_valid_dataframe(self) -> None: """Verify valid DataFrame passes normalization without errors.""" - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) + df = sample_input.create_test_input_dataframe(num_clients=3) result = preprocess.normalize_dataframe(df) @@ -220,7 +172,7 @@ def test_normalize_dataframe_passes_valid_dataframe(self) -> None: def test_normalize_dataframe_handles_missing_values(self) -> None: """Verify NaN/None values in string columns are filled.""" - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) + df = sample_input.create_test_input_dataframe(num_clients=3) df.loc[0, "street_address_line_2"] = None df.loc[1, "postal_code"] = float("nan") @@ -231,7 +183,7 @@ def test_normalize_dataframe_handles_missing_values(self) -> None: def test_normalize_dataframe_converts_dates(self) -> None: """Verify date_of_birth is parsed to datetime.""" - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) + df = sample_input.create_test_input_dataframe(num_clients=2) df["date_of_birth"] = ["2015-01-02", "2014-05-06"] result = preprocess.normalize_dataframe(df) @@ -240,7 +192,7 @@ def test_normalize_dataframe_converts_dates(self) -> None: def test_normalize_dataframe_trims_whitespace(self) -> None: """Verify string columns have leading/trailing whitespace stripped.""" - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df = sample_input.create_test_input_dataframe(num_clients=1) df["first_name"] = [" Alice "] df["last_name"] = [" Zephyr "] @@ -395,7 +347,7 @@ def test_build_result_generates_clients_with_sequences( - Sequence numbers (00001, 00002...) appear on notices - Must be deterministic: same input → same sequences """ - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) + df = sample_input.create_test_input_dataframe(num_clients=3) result = preprocess.build_preprocess_result( df, @@ -419,7 +371,7 @@ def test_build_result_sorts_clients_deterministically( - Required for comparing pipeline runs (reproducibility) - Enables batching by school to work correctly """ - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) + df = sample_input.create_test_input_dataframe(num_clients=3) result1 = preprocess.build_preprocess_result( df, @@ -449,26 +401,23 @@ def test_build_result_sorts_by_school_then_name( - Must be deterministic across pipeline runs - Affects sequence number assignment """ - df = preprocess.map_columns(pd.DataFrame( + df = pd.DataFrame( { - "School Type": ["Public", "Public", "Public", "Public"], - "School Name": ["Zebra School", "Zebra School", "Apple School", "Apple School"], - "Client Id": ["C002", "C001", "C004", "C003"], - "First Name": ["Bob", "Alice", "Diana", "Chloe"], - "Last Name": ["Smith", "Smith", "Jones", "Jones"], - "Age": ["10", "10", "10", "10"], - "Date of Birth": ["2015-01-01", "2015-01-02", "2015-01-03", "2015-01-04"], - "Street Address Line 1": ["123 Main", "123 Main", "123 Main", "123 Main"], - "Street Address Line 2": ["", "", "", ""], - "City": ["Town", "Town", "Town", "Town"], - "Province/Territory": ["ON", "ON", "ON", "ON"], - "Postal Code": ["N1H 2T2", "N1H 2T2", "N1H 2T2", "N1H 2T2"], - "Overdue Disease": ["Measles", "Measles", "Measles", "Measles"], - "Overdue Agent": ["MMR", "MMR", "MMR", "MMR"], - "Imms Given": ["", "", "", ""], - "Birth Year": ["2015", "2015", "2015", "2015"], + "school_name": ["Zebra School", "Zebra School", "Apple School", "Apple School"], + "client_id": ["C002", "C001", "C004", "C003"], + "first_name": ["Bob", "Alice", "Diana", "Chloe"], + "last_name": ["Smith", "Smith", "Jones", "Jones"], + "date_of_birth": ["2015-01-01", "2015-01-02", "2015-01-03", "2015-01-04"], + "street_address_line_1": ["123 Main", "123 Main", "123 Main", "123 Main"], + "street_address_line_2": ["", "", "", ""], + "city": ["Town", "Town", "Town", "Town"], + "province": ["ON", "ON", "ON", "ON"], + "postal_code": ["N1H 2T2", "N1H 2T2", "N1H 2T2", "N1H 2T2"], + "overdue_disease": ["Measles", "Measles", "Measles", "Measles"], + "overdue_agent": ["MMR", "MMR", "MMR", "MMR"], + "imms_given": ["", "", "", ""], } - )) + ) result = preprocess.build_preprocess_result( df, language="en", @@ -491,7 +440,7 @@ def test_build_result_maps_vaccines_correctly( - Vaccine mapping must preserve all components - Affects disease coverage reporting in notices """ - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df = sample_input.create_test_input_dataframe(num_clients=1) df["imms_given"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( @@ -531,7 +480,7 @@ def test_build_result_uses_explicit_config_path( ), encoding="utf-8", ) - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df = sample_input.create_test_input_dataframe(num_clients=1) df["overdue_disease"] = ["DTaP - 2"] df["imms_given"] = ["May 1, 2020 - DTaP"] @@ -559,26 +508,23 @@ def test_build_result_handles_missing_board_name_with_warning( - Should auto-generate board ID and log warning - Allows pipeline to proceed without failing """ - df = preprocess.map_columns(pd.DataFrame( + df = pd.DataFrame( { - "School Type": ["Public"], - "School Name": ["Test School"], - "Client Id": ["C001"], - "First Name": ["Alice"], - "Last Name": ["Zephyr"], - "Age": ["10"], - "Date of Birth": ["2015-01-01"], - "Street Address Line 1": ["123 Main"], - "Street Address Line 2": [""], - "City": ["Guelph"], - "Province/Territory": ["ON"], - "Postal Code": ["N1H 2T2"], - "Overdue Disease": ["Measles"], - "Overdue Agent": ["MMR"], - "Imms Given": [""], - "Birth Year": ["2015"], + "school_name": ["Test School"], + "client_id": ["C001"], + "first_name": ["Alice"], + "last_name": ["Zephyr"], + "date_of_birth": ["2015-01-01"], + "street_address_line_1": ["123 Main"], + "street_address_line_2": [""], + "city": ["Guelph"], + "province": ["ON"], + "postal_code": ["N1H 2T2"], + "overdue_disease": ["Measles"], + "overdue_agent": ["MMR"], + "imms_given": [""], } - )) + ) result = preprocess.build_preprocess_result( df, language="en", @@ -600,7 +546,7 @@ def test_build_result_french_language_support( - Preprocessing must handle both language variants - Dates must convert to French format for display """ - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1, language="fr")) + df = sample_input.create_test_input_dataframe(num_clients=1, language="fr") result = preprocess.build_preprocess_result( df, @@ -621,7 +567,7 @@ def test_build_result_handles_replace_unspecified( - Input may contain "Not Specified" vaccine agents - Pipeline should filter these out to avoid confusing notices """ - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df = sample_input.create_test_input_dataframe(num_clients=1) result = preprocess.build_preprocess_result( df, @@ -642,7 +588,7 @@ def test_build_result_detects_duplicate_client_ids( - Must warn about this data quality issue - Later records with same ID will overwrite earlier ones in notice generation """ - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=2)) + df = sample_input.create_test_input_dataframe(num_clients=2) # Force duplicate client IDs df.loc[0, "client_id"] = "C123456789" df.loc[1, "client_id"] = "C123456789" @@ -673,7 +619,7 @@ def test_build_result_detects_multiple_duplicate_client_ids( - May have multiple different client IDs that are duplicated - Each duplicate set should generate a separate warning """ - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=5)) + df = sample_input.create_test_input_dataframe(num_clients=5) # Create two sets of duplicates df.loc[0, "client_id"] = "C111111111" df.loc[1, "client_id"] = "C111111111" @@ -710,7 +656,7 @@ def test_build_result_no_warning_for_unique_client_ids( Real-world significance: - Normal case with clean data should not produce duplicate warnings """ - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=3)) + df = sample_input.create_test_input_dataframe(num_clients=3) result = preprocess.build_preprocess_result( df, @@ -1293,7 +1239,7 @@ def test_build_result_maps_vaccines_correctly(self, default_vaccine_reference) - Real-world significance: - DTaP → Diphtheria, Tetanus, Pertussis columns populated. """ - df = preprocess.map_columns(sample_input.create_test_input_dataframe(num_clients=1)) + df = sample_input.create_test_input_dataframe(num_clients=1) df["imms_given"] = ["May 1, 2020 - DTaP"] result = preprocess.build_preprocess_result( diff --git a/uv.lock b/uv.lock index a79a5ff..67c78ba 100644 --- a/uv.lock +++ b/uv.lock @@ -483,7 +483,7 @@ wheels = [ [[package]] name = "immuknow" -version = "1.0.0" +version = "1.1.0" source = { editable = "." } dependencies = [ { name = "babel" }, From 474062904307fbba8c17657388d3c1218e71e647 Mon Sep 17 00:00:00 2001 From: TiaTuinstra Date: Thu, 27 Aug 2026 18:40:59 +0000 Subject: [PATCH 6/6] client info completeness check; able to toggle whether address/client info check will drop or retain incomplete records --- CHANGELOG.md | 4 +- docs/reference/pipeline_steps.md | 12 ++++-- pipeline/orchestrator.py | 3 +- pipeline/preprocess.py | 69 ++++++++++++++++++++++++++++++-- tests/unit/test_orchestrator.py | 9 ++++- 5 files changed, 85 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e72bc..2ad8218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [v1.1.0](https://github.com/WDGPH/ImmuKnow/releases/tag/v1.1.0) - 2026-08-25 +## [v1.1.0](https://github.com/WDGPH/ImmuKnow/releases/tag/v1.1.0) - 2026-08-27 [Compare with v1.0.0](https://github.com/WDGPH/ImmuKnow/compare/v1.0.0...v1.1.0) @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Switch input schema from fuzzy matching / data normalization to enforced Frictionless schema validation. Add schema page to mkdocs. - Minimize required columns by deriving where possible. - Adopt `lower_snake_case` column naming. +- Check client information completeness during preprocessing: records missing `first_name`, `last_name`, `date_of_birth`, `client_id`, `school_name`, `overdue_disease`, or `imms_given` are logged and written to `output/incomplete_clients.csv`; like the address completeness check, by default incomplete records are dropped before further processing. +- Address and client info completeness checks able to be toggled to drop or retain incomplete records (e.g. students with incomplete address may still receive their notice through email). ## [v1.0.0](https://github.com/WDGPH/ImmuKnow/releases/tag/v1.0.0) - 2026-08-13 diff --git a/docs/reference/pipeline_steps.md b/docs/reference/pipeline_steps.md index 61ebe52..5e63c86 100644 --- a/docs/reference/pipeline_steps.md +++ b/docs/reference/pipeline_steps.md @@ -57,6 +57,8 @@ Reads the raw Excel input, validates the schema, normalizes all client and vacci - `output/artifacts/preprocessed_clients_.json` — canonical client artifact - `output/logs/preprocess_.log` — processing log +- `output/incomplete_addresses.csv` — records dropped due to missing address fields (written when any are found) +- `output/incomplete_clients.csv` — records with missing required client fields, retained in processing (written when any are found) - `phix_exact.csv`, `phix_inexact.csv`, `phix_no_match.csv` — school match audit CSVs (when PHIX validation enabled) **Processing:** @@ -67,10 +69,12 @@ Reads the raw Excel input, validates the schema, normalizes all client and vacci 4. Expands vaccine codes to disease names using `vaccine_reference.json` 5. Filters diseases against `chart_diseases_header`; collapses unlisted diseases to "Other" 6. Computes client ages relative to `date_notice_delivery` (determines parent vs. student addressing) -7. Sorts clients deterministically: school → last name → first name → client ID -8. Assigns stable sequence numbers (`00001`, `00002`, …) -9. Synthesizes missing school/board identifiers where needed -10. Writes the canonical JSON artifact +7. Checks address completeness: records missing `address`, `city`, `province`, or `postal_code` are logged, written to `output/incomplete_addresses.csv`, and **dropped** by default from further processing +8. Checks client information completeness: records missing `first_name`, `last_name`, `date_of_birth`, `client_id`, `school_name`, `overdue_disease`, or `imms_given` are logged and written to `output/incomplete_clients.csv`, and **dropped** by default from further processing +9. Sorts clients deterministically: school → last name → first name → client ID +10. Assigns stable sequence numbers (`00001`, `00002`, …) +11. Synthesizes missing school/board identifiers where needed +12. Writes the canonical JSON artifact --- diff --git a/pipeline/orchestrator.py b/pipeline/orchestrator.py index 0bf3d40..4a18d2f 100755 --- a/pipeline/orchestrator.py +++ b/pipeline/orchestrator.py @@ -229,7 +229,8 @@ def run_step_2_preprocess( df = preprocess.normalize_dataframe(df_raw) # Check that addresses are complete, return only complete rows - df = preprocess.check_addresses_complete(df) + df = preprocess.check_addresses_complete(df, drop_incomplete=True) + df = preprocess.check_client_info_complete(df, drop_incomplete=True) # Validate schools against PHIX mapping df, phix_warnings = preprocess.run_phix_validation(df, output_dir) diff --git a/pipeline/preprocess.py b/pipeline/preprocess.py index 715b6e3..ff0e00c 100644 --- a/pipeline/preprocess.py +++ b/pipeline/preprocess.py @@ -161,11 +161,11 @@ def format_iso_date_for_language(iso_date: str, language: str) -> str: return format_date(date_obj, format="long", locale=locale) -def check_addresses_complete(df: pd.DataFrame) -> pd.DataFrame: +def check_addresses_complete(df: pd.DataFrame, drop_incomplete=True) -> pd.DataFrame: """ Check if address fields are complete in the DataFrame. - Adds a boolean 'address_complete' column based on presence of + Adds a temporary boolean 'address_complete' column based on presence of street address, city, province, and postal code. """ @@ -213,8 +213,69 @@ def check_addresses_complete(df: pd.DataFrame) -> pd.DataFrame: incomplete_records.to_csv(incomplete_path, index=False) LOG.info("Incomplete address records written to %s", incomplete_path) - # Return only rows with complete addresses - return df.loc[df["address_complete"]].drop(columns=["address_complete"]) + # Return only rows with complete addresses based on drop_incomplete flag + if drop_incomplete: + return df.loc[df["address_complete"]].drop(columns=["address_complete"]) + else: + return df.drop(columns=["address_complete"]) + + +def check_client_info_complete(df: pd.DataFrame, drop_incomplete=True) -> pd.DataFrame: + """ + Check if client fields are complete in the DataFrame. + + Adds a temporary boolean 'client_info_complete' column based on presence of + first name, last name, DOB, school name, overdue disease, immunizations given, and client ID. + """ + + df = df.copy() + + # Normalize text fields: convert to string, strip whitespace, convert "" to NA + client_info_cols = [ + "school_name", + "client_id", + "first_name", + "last_name", + "date_of_birth", + "overdue_disease", + "imms_given", + ] + + for col in client_info_cols: + df[col] = df[col].astype(str).str.strip().replace({"": pd.NA, "nan": pd.NA}) + + # Check completeness + df["client_info_complete"] = ( + df["first_name"].notna() + & df["last_name"].notna() + & df["client_id"].notna() + & df["date_of_birth"].notna() + & df["overdue_disease"].notna() + & df["imms_given"].notna() + ) + + if not df["client_info_complete"].all(): + incomplete_count = (~df["client_info_complete"]).sum() + LOG.warning( + "There are %d records with incomplete/invalid client information.", + incomplete_count, + ) + print( + f"⚠️ There are {incomplete_count} total records with incomplete/invalid client information." + ) + + incomplete_records = df.loc[~df["client_info_complete"]] + + incomplete_path = Path("output/incomplete_clients.csv") + incomplete_records.to_csv(incomplete_path, index=False) + LOG.info("Incomplete client records written to %s", incomplete_path) + print(f"Incomplete client records written to {incomplete_path}") + + # Return only rows with complete client info based on drop_incomplete flag + if drop_incomplete: + return df.loc[df["client_info_complete"]].drop(columns=["client_info_complete"]) + else: + return df.drop(columns=["client_info_complete"]) def convert_date_iso(date_str: str) -> str: diff --git a/tests/unit/test_orchestrator.py b/tests/unit/test_orchestrator.py index e4bdf21..171bf82 100644 --- a/tests/unit/test_orchestrator.py +++ b/tests/unit/test_orchestrator.py @@ -236,7 +236,11 @@ def test_run_step_2_passes_selected_config_path(self, tmp_path: Path) -> None: patch( "pipeline.orchestrator.preprocess.check_addresses_complete", return_value=MagicMock(), - ) as mock_check_addresses, + ), + patch( + "pipeline.orchestrator.preprocess.check_client_info_complete", + return_value=MagicMock(), + ) as mock_check_client_info, patch( "pipeline.orchestrator.preprocess.build_preprocess_result", return_value=result, @@ -257,7 +261,8 @@ def test_run_step_2_passes_selected_config_path(self, tmp_path: Path) -> None: ) assert total_clients == 0 - assert mock_build_result.call_args.args[0] is mock_check_addresses.return_value + assert mock_build_result.call_args.args[0] is mock_check_client_info.return_value + assert mock_build_result.call_args.kwargs["config_path"] == ( config_dir / "parameters.yaml" )