"""Parsing a date out of a request body, strictly. WHY THIS EXISTS Two plugins carried an identical private `_parse_date` that swallowed the error: try: return datetime.strptime(str(value)[:10], '%Y-%m-%d').date() except (ValueError, TypeError): return None So an unparseable date and a deliberately cleared one produced the same result, and the write went ahead. `PUT /api/warranty/` with enddate 'not-a-date' answered 200 "Warranty updated" and stored NULL - the warranty then vanished from every expiry surface, silently, on the strength of a typo. The measuring-tool calibration date did the same thing and dropped the tool out of the calibration report. The rule here: an EXPLICIT empty value clears the field, and anything that is not a date is refused. Those are different intents and they get different answers. Callers turn `DateValueError` into a 400 naming the field, so the person who mistyped it finds out at the point of the mistake rather than at the next audit. Accepted forms are deliberately narrow - 'YYYY-MM-DD', and the ISO-8601 datetime forms whose leading ten characters are a date, since that is what the frontend's date inputs and every vendor API in the tree emit. Anything looser is a guess about what somebody meant. """ from datetime import date, datetime class DateValueError(ValueError): """A supplied value was neither empty nor a date this accepts. Carries the field name so the error message can say which one, which is the whole point of refusing rather than nulling. """ def __init__(self, field, value): self.field = field self.value = value super().__init__( '%s must be a date as YYYY-MM-DD (got %r)' % (field, value)) def parse_date(value, field='date'): """Return a date, or None for an explicitly empty value. Raises DateValueError for anything else. None and '' mean "clear it"; they are the only values that produce None. """ if value is None: return None if isinstance(value, datetime): return value.date() if isinstance(value, date): return value if not isinstance(value, str): # A number or a list is not a mistyped date, it is the wrong type. raise DateValueError(field, value) text = value.strip() if not text: return None try: return datetime.strptime(text[:10], '%Y-%m-%d').date() except ValueError: raise DateValueError(field, value) def parse_date_fields(data, fields): """Parse several date fields out of a payload at once. Only keys PRESENT in the payload are returned, so a caller can tell "not mentioned" (leave it alone) from "sent as empty" (clear it) - a distinction a plain `data.get(...)` throws away, and the reason the alias-clearing bug existed on the PC form. """ return {field: parse_date(data[field], field) for field in fields if field in data}