"""Opt-in standard HTTP frame protocol for privately managed devices.""" import requests from pigway_frame_flow.errors import ErrorCode as E from pigway_frame_flow.errors import FlowError from pigway_frame_flow.models import Device, GalleryImage from pigway_frame_flow.settings import secret class GenericHTTPDriver: def __init__(self, id, entry, settings): from urllib.parse import urlsplit self.id, self.entry, self.settings = id, entry, settings self.endpoint = entry.get("endpoint", "").rstrip("/") parsed = urlsplit(self.endpoint) if ( parsed.scheme not in ("https", "http") or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment ): raise ValueError("Invalid generic frame endpoint") if parsed.scheme == "http" and not entry.get("allow_insecure_lan", False): raise ValueError("HTTP frames require explicit allow_insecure_lan") self.token = secret(entry.get("token_env", "FRAME_FLOW_HTTP_FRAME_TOKEN")) settings.additional_secrets.append(self.token) def call(self, method, path, **kwargs): writing = method != "GET" if writing and not self.settings.write_enabled: raise FlowError(E.WRITE_DISABLED, "Device writes are disabled", 403) try: response = requests.request( method, self.endpoint + path, headers={"Authorization": "Bearer " + self.token} if self.token else {}, timeout=10, allow_redirects=False, **kwargs, ) response.raise_for_status() return response except requests.RequestException: raise FlowError( E.WRITE_RESULT_UNKNOWN if writing else E.CLOUD_UNAVAILABLE, "HTTP frame request failed", 409 if writing else 502, ) from None def invalidate(self): pass def device(self, refresh=False): raw = self.call("GET", "/device").json() raw["id"] = self.id raw["driver"] = "generic_http_frame" return Device.model_validate(raw) def get_current_image(self): return self.device().current_content.id def health(self): try: self.device() return True except FlowError: return False def images(self): return [ GalleryImage.model_validate(x) for x in self.call("GET", "/images").json() ] def upload(self, path, name): with path.open("rb") as f: return int( self.call( "POST", "/images", files={"file": (name, f, "image/jpeg")} ).json()["image_id"] ) def display(self, image_id): self.call("POST", "/display", json={"image_id": image_id}) def delete(self, image_id): self.call("DELETE", f"/images/{int(image_id)}") def download(self, image_id): response = self.call("GET", f"/images/{int(image_id)}/content", stream=True) try: parts = [] total = 0 for chunk in response.iter_content(65536): total += len(chunk) if total > self.settings.max_bytes: raise FlowError(E.INVALID_REQUEST, "Image exceeds size limit") parts.append(chunk) return b"".join(parts) finally: response.close() def display_by_name(self, name): matches = [x for x in self.images() if x.name == name] if len(matches) != 1: raise FlowError(E.CONFLICT, "Filename must match exactly one image", 409) self.display(matches[0].image_id)