CoolFace
Apppublic

wfr2/species-synonym-api

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
schema.py262 linesDownload Raw Back to utils
1import re2 3import pandas as pd4 5from scripts.config import ALL_PORTALS6 7# value to use when data is unavailable (i.e. never present) from a given API source. This is not the same as an empty string, which indicates that the data was not found for that particular query (e.g. no author for a given taxonomic name).8UNAVAILABLE = "N/A"9 10SYNONYM_COLUMNS = [11    "api_name",  # name of API source that provided the data, e.g. GBIF (required)12    "kingdom",  # taxonomic kingdom (optional)13    "phylum",  # taxonomic phylum (optional)14    "class",  # taxonomic class (optional)15    "family",  # taxonomic family (optional)16    "order",  # taxonomic order (optional)17    "subfamily",  # taxonomic subfamily (optional)18    "genus",  # taxonomic genus (required)19    "species",  # taxonomic species (required)20    "author",  # author of the taxonomic name (optional)21    "publication_name",  # name of the publication where the taxonomic name was published (optional)22    "publication_year",  # year when the taxonomic name was published (optional)23    "status",  # status of the taxonomic name in the API source's database, either "Accepted" or "Synonym" (optional)24    "original_source",  # name of the source that provided the data to the API source, e.g. a cited journal article, museum collection, etc. (optional)25    "api_link",  # link to the search result on the API source's website (optional)26    "api_internal_id",  # unique identifier for the record of this taxonomic name in the API source's database (required)27]28 29# valid values for the "status" column, including UNAVAILABLE to indicate that the API source does not provide this information30_STATUS_VALUES = {31    "Accepted",32    "Synonym",33    "",34    UNAVAILABLE,35}36 37# columns that represent taxonomic ranks and must be single words (no whitespace) or UNAVAILABLE38_TAXON_COLUMNS = {39    "kingdom",40    "phylum",41    "class",42    "order",43    "family",44    "subfamily",45    "genus",46    "species",47}48 49# columns that must be strings (can be empty or UNAVAILABLE, but not other types)50_STRING_COLUMNS = {51    "api_internal_id",52    "original_source",53    "author",54    "publication_name",55}56 57# columns that must be explicitly provided (cannot be UNAVAILABLE)58_REQUIRED_COLUMNS = {59    "api_name",60    "genus",61    "species",62    "api_internal_id",63}64 65_API_NAMES: set[str] = {p.display_name for p in ALL_PORTALS}66 67 68def _make_string_validator(col: str):69    """70    Create a validator function that checks if a value is a string.71 72    Parameters73    ----------74    col : str75        Column name, used in the error message.76 77    Returns78    -------79    callable80        Validator that raises ``ValueError`` if the value is not a string.81    """82 83    def validate(v) -> None:84        if not isinstance(v, str):85            raise ValueError(f"'{col}' must be a string, got {v!r}")86 87    return validate88 89 90def _make_taxon_validator(col: str):91    """92    Create a validator function that checks if a value is a single word or ``UNAVAILABLE``. All taxon entries must be a single word (no whitespace) or ``UNAVAILABLE``.93 94    Parameters95    ----------96    col : str97        Column name, used in the error message.98 99    Returns100    -------101    callable102        Validator that raises ``ValueError`` if the value is not a string, contains103        whitespace, or is not equal to ``UNAVAILABLE``.104    """105 106    def validate(v: str) -> None:107        if not isinstance(v, str) or (v != UNAVAILABLE and re.search(r"\s", v)):108            raise ValueError(109                f"'{col}' must be a single word (no whitespace) or {UNAVAILABLE!r}, got {v!r}"110            )111 112    return validate113 114 115def _validate_api_name(v: str) -> None:116    """117    Validate that an api name is one of the allowed values in ``_API_NAMES``.118 119    Parameters120    ----------121    v : str122        Value to validate.123 124    Raises125    ------126    ValueError127        If ``v`` is not in ``_API_NAMES``.128    """129    if v not in _API_NAMES:130        raise ValueError(f"'api_name' must be one of {_API_NAMES!r}, got {v!r}")131 132 133def _validate_publication_year(v: str) -> None:134    """135    Validate that a publication year is a 4-digit year string, ``""``, or ``UNAVAILABLE``.136 137    Parameters138    ----------139    v : str140        Value to validate.141 142    Raises143    ------144    ValueError145        If ``v`` is not a 4-digit numeric string, ``""``, or ``UNAVAILABLE``.146    """147    if v in (UNAVAILABLE, ""):148        return149    if not re.fullmatch(r"\d{4}", v):150        raise ValueError(151            f"'publication_year' must be a 4-digit numeric string, an empty string, or {UNAVAILABLE!r}, got {v!r}"152        )153 154 155def _validate_api_link(v: str) -> None:156    """157    Validate that an api link starts with ``http://`` or ``https://``, or is ``UNAVAILABLE``.158 159    Parameters160    ----------161    v : str162        Value to validate.163 164    Raises165    ------166    ValueError167        If ``v`` does not start with ``http://`` or ``https://`` and is not equal to ``UNAVAILABLE``.168    """169    if v != UNAVAILABLE and v != "" and not re.match(r"https?://", v):170        raise ValueError(171            f"'api_link' must start with 'http://' or 'https://', or be {UNAVAILABLE!r}, got {v!r}"172        )173 174 175def _validate_status(v: str) -> None:176    """177    Validate that a status value is one of the allowed values in ``_STATUS_VALUES``.178 179    Parameters180    ----------181    v : str182        Value to validate.183 184    Raises185    ------186    ValueError187        If ``v`` is not in ``_STATUS_VALUES``.188    """189    if v not in _STATUS_VALUES:190        raise ValueError(f"'status' must be one of {_STATUS_VALUES}, got {v!r}")191 192 193# mapping of column name to validator function for validating synonym row values. All columns have validators, but some share the same validator (e.g. all taxon columns use the same taxon validator factory).194_VALIDATORS = {195    "api_name": _validate_api_name,196    "publication_year": _validate_publication_year,197    "api_link": _validate_api_link,198    "status": _validate_status,199    **{col: _make_taxon_validator(col) for col in _TAXON_COLUMNS},200    **{col: _make_string_validator(col) for col in _STRING_COLUMNS},201}202 203 204def empty_synonym_table() -> pd.DataFrame:205    """206    Create an empty DataFrame with the synonym table columns.207 208    Returns209    -------210    pd.DataFrame211        Empty DataFrame with columns defined by ``SYNONYM_COLUMNS``.212    """213    return pd.DataFrame(columns=SYNONYM_COLUMNS)214 215 216def make_synonym_row(**kwargs) -> dict:217    """218    Build a validated synonym row dict with all columns from ``SYNONYM_COLUMNS``.219 220    Any column not provided in ``kwargs`` defaults to ``UNAVAILABLE``. Required221    columns (``_REQUIRED_COLUMNS``) must be explicitly provided and cannot be222    ``UNAVAILABLE``. All values are validated against ``_VALIDATORS`` before the223    row is returned.224 225    Parameters226    ----------227    **kwargs228        Column values keyed by column name. Valid keys are those in ``SYNONYM_COLUMNS``.229 230    Returns231    -------232    dict233        Mapping of column name to value for all columns in ``SYNONYM_COLUMNS``.234 235    Raises236    ------237    ValueError238        If a required column is missing or set to ``UNAVAILABLE``, or if any239        column value fails its validator.240    """241    # Validate that a blank string, "", was used for any passed value that did not have an entry, not None or "N/A"242    for col, v in kwargs.items():243        if v is None:244            raise TypeError(245                f"Got None for '{col}' in make_synonym_row. "246                f"Pass '' if the field was searched but not found, "247                f"or omit the argument if the API does not provide this field."248            )249        if isinstance(v, str) and v == UNAVAILABLE:250            raise ValueError(251                f"Got {UNAVAILABLE!r} for '{col}' in make_synonym_row. "252                f"Do not pass {UNAVAILABLE!r} directly — "253                f"omit the argument to let make_synonym_row apply the default."254            )255    row = {col: kwargs.get(col, UNAVAILABLE) for col in SYNONYM_COLUMNS}256    missing = [col for col in _REQUIRED_COLUMNS if row[col] in (UNAVAILABLE, "")]257    if missing:258        raise ValueError(f"Missing required columns: {sorted(missing)}")259    for col, validate in _VALIDATORS.items():260        validate(row[col])261    return row262