Source code for aviary.render

import json
import logging
import os
import shutil
from pathlib import Path
from typing import Any
from uuid import UUID, uuid4

from pydantic import BaseModel, Field, field_validator

from aviary.env import Frame

try:
    from boto3 import client
except ImportError:
    client = None  # type: ignore[assignment]

logger = logging.getLogger(__name__)


[docs] class Renderer(BaseModel): id: UUID | int | str = Field( default_factory=lambda: str(uuid4()).replace("-", "")[:16] ) frames: list[Frame] = [] prefix: str name: str = Field( default="Trajectory", description="Name of the renderer, used in the manifest file.", )
[docs] @field_validator("prefix") @classmethod def check_prefix_is_alphanum(cls, v: str) -> str: if not v.isalnum(): raise ValueError("Prefix must be an alphanumeric string") return v
[docs] def append(self, frame: Frame) -> None: self.frames.append(frame)
def _make_filename(self, index: int) -> str: return f"{self.prefix}_{self.id!s}_{index}.json" def _render(self) -> dict[str, dict[str, Any]]: """Get a mapping of filenames to serialized frame with renderer metadata.""" return { self._make_filename(i): frame.model_dump() | { "index": i + 1, "prev_frame": self._make_filename(i - 1) if i > 0 else None, "next_frame": ( self._make_filename(i + 1) if i + 1 < len(self.frames) else None ), } for i, frame in enumerate(self.frames) }
[docs] def build( # noqa: C901 self, build_dir: str | os.PathLike, indent: int = 4, r2_bucket: str | None = None, extra_files: list[str | os.PathLike] | None = None, ) -> None: name_to_data = self._render() if not name_to_data: logger.warning("No frames to render.") return build_dir_path = Path(build_dir) file_list = [] for name, data in name_to_data.items(): path = build_dir_path / name path.parent.mkdir(parents=True, exist_ok=True) with path.open("w") as f: json.dump(data, f, indent=indent) file_list.append(name) # now write manifest (summary) file, and we know name exists # because we check name_to_data has values # pylint: disable-next=undefined-loop-variable first_name, last_name = next(iter(name_to_data.keys())), name # NOTE: we have the '-info' prefix so that we can use a prefix-filter manifest_fn = f"{self.prefix}_info_{self.id!s}.json" with (build_dir_path / manifest_fn).open("w") as f: json.dump( {"name": self.name, "first": first_name, "last": last_name}, f, indent=indent, ) file_list.append(manifest_fn) # copy the extra files if extra_files: for p in extra_files: path = Path(p) output_path = build_dir_path / path.name output_path.parent.mkdir(parents=True, exist_ok=True) try: shutil.copyfile(path, output_path) file_list.append(output_path.name) except FileNotFoundError: logger.warning(f"Failed to copy file from {path} to {output_path}.") if r2_bucket: # Upload to bucket try: CF_ACCOUNT_ID = os.environ["CF_ACCOUNT_ID"] CF_ACCESS_KEY_ID = os.environ["CF_ACCESS_KEY_ID"] CF_SECRET_ACCESS_KEY = os.environ["CF_SECRET_ACCESS_KEY"] if not (CF_ACCOUNT_ID and CF_ACCESS_KEY_ID and CF_SECRET_ACCESS_KEY): raise ValueError("Empty Cloudflare R2 credentials") # noqa: TRY301 except (KeyError, ValueError) as exc: raise ValueError("Cloudflare R2 credentials unset.") from exc try: s3 = client( service_name="s3", endpoint_url=f"https://{CF_ACCOUNT_ID}.r2.cloudflarestorage.com", aws_access_key_id=CF_ACCESS_KEY_ID, aws_secret_access_key=CF_SECRET_ACCESS_KEY, region_name="auto", ) except TypeError: raise ImportError( "Bucket uploads requires the 'cloud' extra for 'boto3'. Please:" " `pip install aviary[cloud]`." ) from None for fn in file_list: with (build_dir_path / fn).open("rb") as d: s3.upload_fileobj(d, r2_bucket, fn)