Download validation.py from drmarolla/rmti: direct link, hf CLI and curl.
- Browser
- Download file 3.76 kB
-
https://huggingface.co/spaces/drmarolla/rmti/resolve/main/validation.py
- Command line
-
hf download hf://spaces/drmarolla/rmti/validation.py
-
curl -L -o validation.py https://huggingface.co/spaces/drmarolla/rmti/resolve/main/validation.py
3.76 kB
| """Strict input validation for the RMTI API. | |
| Design rules: | |
| * Every request body is checked against an explicit allowlist of fields. | |
| Unknown fields are rejected (HTTP 400), not silently ignored. This also | |
| acts as a guard against someone accidentally posting identifiers | |
| (names, record numbers, dates of birth) to the oncology endpoints. | |
| * Every numeric input is range-checked BEFORE it reaches the framework | |
| code, because core.rmti_core.score() itself does not validate ranges. | |
| * Booleans, NaN, Infinity and strings-that-look-like-numbers are rejected. | |
| """ | |
| import math | |
| MAX_ITEMS = 100 # rows per /rank call | |
| MAX_HAZARDS = 10 # hazards per /multi call | |
| MAX_LABEL_LEN = 60 | |
| N_HARD_MAX = 10_000_000_000 # more than the world population; sanity bound | |
| class ApiError(Exception): | |
| def __init__(self, status, code, message): | |
| super().__init__(message) | |
| self.status, self.code, self.message = status, code, message | |
| def bad(code, message): | |
| return ApiError(400, code, message) | |
| def require_object(body, what="request body"): | |
| if not isinstance(body, dict): | |
| raise bad("invalid_body", f"{what} must be a JSON object") | |
| return body | |
| def check_fields(d, allowed, where="request"): | |
| unknown = sorted(set(d) - set(allowed)) | |
| if unknown: | |
| raise bad("unknown_field", | |
| f"Unexpected field(s) in {where}: {', '.join(map(str, unknown))}. " | |
| f"Allowed: {', '.join(sorted(allowed))}. " | |
| "Do not submit names, record numbers or other identifiers.") | |
| def num(d, key, lo, hi, *, required=True, default=None, where="request"): | |
| if key not in d: | |
| if required: | |
| raise bad("missing_field", f"'{key}' is required in {where}") | |
| return default | |
| v = d[key] | |
| if isinstance(v, bool) or not isinstance(v, (int, float)): | |
| raise bad("invalid_type", f"'{key}' must be a number in {where}") | |
| if not math.isfinite(v): | |
| raise bad("invalid_value", f"'{key}' must be finite in {where}") | |
| if v < lo or v > hi: | |
| raise bad("out_of_range", f"'{key}'={v} is outside [{lo}, {hi}] in {where}") | |
| return v | |
| def integer_in(d, key, choices, *, required=True, default=None, where="request"): | |
| if key not in d: | |
| if required: | |
| raise bad("missing_field", f"'{key}' is required in {where}") | |
| return default | |
| v = d[key] | |
| if isinstance(v, bool) or not isinstance(v, (int, float)) or v != int(v): | |
| raise bad("invalid_type", f"'{key}' must be an integer in {where}") | |
| if int(v) not in choices: | |
| raise bad("out_of_range", f"'{key}'={v} must be one of {sorted(choices)} in {where}") | |
| return int(v) | |
| def label(d, where="item"): | |
| v = d.get("label") | |
| if not isinstance(v, str) or not (1 <= len(v) <= MAX_LABEL_LEN): | |
| raise bad("invalid_label", f"'label' must be a string of 1-{MAX_LABEL_LEN} characters in {where}") | |
| if any(ord(c) < 32 for c in v): | |
| raise bad("invalid_label", f"'label' must not contain control characters in {where}") | |
| return v | |
| def rho_pair(rho_now, rho_after, where): | |
| """rho_now == 1 would divide by zero in the risk-reduction formula.""" | |
| if rho_after is not None and rho_now >= 1: | |
| raise bad("invalid_value", | |
| f"'rho_now' must be < 1 when 'rho_after' is given (risk reduction is undefined) in {where}") | |
| def items_list(body): | |
| require_object(body) | |
| check_fields(body, {"items"}, "request body") | |
| items = body.get("items") | |
| if not isinstance(items, list) or not items: | |
| raise bad("invalid_body", "'items' must be a non-empty array") | |
| if len(items) > MAX_ITEMS: | |
| raise bad("too_many_items", f"'items' may contain at most {MAX_ITEMS} entries") | |
| return items | |