dodil/erp-brick-time-off
DODIL ERP brick: time off
src/brick_time_off/domain/evidence.py
"""A document that backs an absence (a fit note, a MATB1): what may be stored, its key, how long it is kept. Plain Python, no I/O."""
import base64
import binascii
import hashlib
from datetime import date
from brick_time_off.domain.errors import RuleBroken
KINDS = {"image/jpeg": ("jpg", b"\xff\xd8\xff"), "image/png": ("png", b"\x89PNG\r\n\x1a\n"), "application/pdf": ("pdf", b"%PDF-")}
MAX_BYTES = 3 * 1024 * 1024
RETAIN_YEARS = 3 # GAP: the spec gives retain_until no rule; three years after the days covered is HMRC's SSP record rule - to be confirmed
def decode(content_type: str, content_base64: str) -> bytes:
"""The document's bytes, or the rule it breaks: a supported type whose first bytes agree, valid base64, not empty, not over 3 MiB."""
if content_type not in KINDS:
raise RuleBroken(f"a fit note is {', '.join(KINDS)}, not {content_type}")
try:
body = base64.b64decode(content_base64, validate=True)
except (binascii.Error, ValueError):
raise RuleBroken("the document is not valid base64") from None
if not body:
raise RuleBroken("the document is empty")
if len(body) > MAX_BYTES:
raise RuleBroken(f"a document is at most {MAX_BYTES // (1024 * 1024)} MiB ({len(body)} bytes given)")
if not body.startswith(KINDS[content_type][1]):
raise RuleBroken(f"the bytes are not a {content_type} (they do not start like one)")
return body
def sha256(body: bytes) -> str:
return hashlib.sha256(body).hexdigest()
def object_key(absence_ref: str, digest: str, content_type: str) -> str:
return f"raw/time-off/{absence_ref}/{digest}.{KINDS[content_type][0]}"
def retain_until(covers_to: date) -> date:
try:
return covers_to.replace(year=covers_to.year + RETAIN_YEARS)
except ValueError: # 29 February
return covers_to.replace(year=covers_to.year + RETAIN_YEARS, day=28)