Source code for lifegraph.serialization

import datetime
import json
from pathlib import Path

from lifegraph.configuration import Papersize
from lifegraph.core import Point, Side


CONFIG_VERSION = 1


def _infer_format(path):
    ext = Path(path).suffix.lower()
    if ext == ".json":
        return "json"
    if ext in (".yaml", ".yml"):
        return "yaml"
    raise ValueError(f"Unsupported config file extension '{ext}'. Use .json, .yaml, or .yml.")


def _get_yaml():
    try:
        import yaml
        return yaml
    except ImportError:
        raise ImportError(
            "PyYAML is required for YAML support. Install it with: pip install pyyaml"
        ) from None


# ---------------------------------------------------------------------------
# Serialization helpers
# ---------------------------------------------------------------------------

def _serialize_date(d):
    return d.isoformat()


def _serialize_color(c):
    if isinstance(c, tuple):
        return list(c)
    return c


def _serialize_hint(h):
    if h is None:
        return None
    if isinstance(h, Point):
        return [h.x, h.y]
    if isinstance(h, (list, tuple)):
        return [h[0], h[1]]
    return None


def _serialize_side(s):
    if s is None:
        return None
    if isinstance(s, Side):
        return s.name.lower()
    return s


def _build_config_dict(graph, include_styling):
    d = {
        "version": CONFIG_VERSION,
        "birthdate": _serialize_date(graph.birthdate),
    }

    if graph.ymax != 90:
        d["max_age"] = graph.ymax

    if graph.min_age != 0:
        d["min_age"] = graph.min_age

    if graph.settings.size != Papersize.A3:
        d["size"] = graph.settings.size.name

    if graph.settings.rcParams["figure.dpi"] != 300:
        d["dpi"] = graph.settings.rcParams["figure.dpi"]

    if graph.label_space_epsilon != 0.2:
        d["label_space_epsilon"] = graph.label_space_epsilon

    if graph.title is not None:
        if graph.title_fontsize is not None:
            d["title"] = {"text": graph.title, "fontsize": graph.title_fontsize}
        else:
            d["title"] = graph.title

    if graph.watermark_text is not None:
        d["watermark"] = graph.watermark_text

    if graph.image_name is not None:
        img = {"path": graph.image_name}
        if graph.image_alpha != 1:
            img["alpha"] = graph.image_alpha
        d["image"] = img

    if graph.draw_max_age:
        d["show_max_age_label"] = True

    if graph._event_records:
        events = []
        for rec in graph._event_records:
            ev = {
                "text": rec["text"],
                "date": _serialize_date(rec["date"]),
                "color": _serialize_color(rec["color"]),
            }
            hint = _serialize_hint(rec.get("hint"))
            if hint is not None:
                ev["hint"] = hint
            side = _serialize_side(rec.get("side"))
            if side is not None:
                ev["side"] = side
            if not rec.get("color_square", True):
                ev["color_square"] = False
            events.append(ev)
        d["events"] = events

    if graph._era_records:
        eras = []
        for rec in graph._era_records:
            era = {
                "text": rec["text"],
                "start_date": _serialize_date(rec["start_date"]),
                "end_date": _serialize_date(rec["end_date"]),
                "color": _serialize_color(rec["color"]),
            }
            side = _serialize_side(rec.get("side"))
            if side is not None:
                era["side"] = side
            if rec.get("alpha") != 0.3:
                era["alpha"] = rec["alpha"]
            eras.append(era)
        d["eras"] = eras

    if graph._era_span_records:
        spans = []
        for rec in graph._era_span_records:
            span = {
                "text": rec["text"],
                "start_date": _serialize_date(rec["start_date"]),
                "end_date": _serialize_date(rec["end_date"]),
                "color": _serialize_color(rec["color"]),
            }
            hint = _serialize_hint(rec.get("hint"))
            if hint is not None:
                span["hint"] = hint
            side = _serialize_side(rec.get("side"))
            if side is not None:
                span["side"] = side
            if rec.get("color_start_and_end_markers"):
                span["color_start_and_end_markers"] = True
            spans.append(span)
        d["era_spans"] = spans

    if include_styling:
        styling = {}

        x_ax = {}
        if graph.xaxis_label != r'Week of the Year $\longrightarrow$':
            x_ax["text"] = graph.xaxis_label
        x_pos = graph.settings.otherParams["xlabel.position"]
        if x_pos != (0.20, 1.05):
            x_ax["positionx"] = x_pos[0]
            x_ax["positiony"] = x_pos[1]
        if graph.settings.otherParams["xlabel.color"] is not None:
            x_ax["color"] = _serialize_color(graph.settings.otherParams["xlabel.color"])
        if graph.settings.otherParams["xlabel.fontsize"] is not None:
            x_ax["fontsize"] = graph.settings.otherParams["xlabel.fontsize"]
        if x_ax:
            styling["x_axis"] = x_ax

        y_ax = {}
        if graph.yaxis_label != r'$\longleftarrow$ Age':
            y_ax["text"] = graph.yaxis_label
        y_pos = graph.settings.otherParams["ylabel.position"]
        if y_pos != (-0.03, 0.95):
            y_ax["positionx"] = y_pos[0]
            y_ax["positiony"] = y_pos[1]
        if graph.settings.otherParams["ylabel.color"] is not None:
            y_ax["color"] = _serialize_color(graph.settings.otherParams["ylabel.color"])
        if graph.settings.otherParams["ylabel.fontsize"] is not None:
            y_ax["fontsize"] = graph.settings.otherParams["ylabel.fontsize"]
        if y_ax:
            styling["y_axis"] = y_ax

        if styling:
            d["styling"] = styling

    return d


[docs] def export_config(graph, path, include_styling=False): fmt = _infer_format(path) d = _build_config_dict(graph, include_styling) with open(path, "w") as f: if fmt == "json": json.dump(d, f, indent=2) else: yaml = _get_yaml() yaml.dump(d, f, default_flow_style=False, sort_keys=False)
# --------------------------------------------------------------------------- # Deserialization helpers # --------------------------------------------------------------------------- def _parse_date(s): return datetime.date.fromisoformat(s) def _parse_color(c): if isinstance(c, list): return tuple(c) return c def _parse_hint(h): if h is None: return None if isinstance(h, list): return Point(h[0], h[1]) return h def _parse_side(s): if s is None: return None if isinstance(s, str): return Side[s.upper()] return s def _parse_papersize(s): if s is None: return Papersize.A3 if isinstance(s, str): return Papersize[s] return s
[docs] def import_config(cls, path, apply_styling=True): fmt = _infer_format(path) with open(path) as f: if fmt == "json": d = json.load(f) else: yaml = _get_yaml() d = yaml.safe_load(f) version = d.get("version", 1) if version != CONFIG_VERSION: raise ValueError(f"Unsupported config version {version}. Expected {CONFIG_VERSION}.") birthdate = _parse_date(d["birthdate"]) size = _parse_papersize(d.get("size")) dpi = d.get("dpi", 300) label_space_epsilon = d.get("label_space_epsilon", 0.2) max_age = d.get("max_age", 90) min_age = d.get("min_age", 0) graph = cls(birthdate, size=size, dpi=dpi, label_space_epsilon=label_space_epsilon, max_age=max_age, min_age=min_age) # Title title = d.get("title") if title is not None: if isinstance(title, str): graph.add_title(title) elif isinstance(title, dict): graph.add_title(title["text"], fontsize=title.get("fontsize")) # Watermark watermark = d.get("watermark") if watermark is not None: graph.add_watermark(watermark) # Image image = d.get("image") if image is not None: graph.add_image(image["path"], alpha=image.get("alpha", 1)) # Show max age label if d.get("show_max_age_label"): graph.show_max_age_label() # Events for ev in d.get("events", []): graph.add_life_event( text=ev["text"], date=_parse_date(ev["date"]), color=_parse_color(ev.get("color")), hint=_parse_hint(ev.get("hint")), side=_parse_side(ev.get("side")), color_square=ev.get("color_square", True), ) # Eras for era in d.get("eras", []): graph.add_era( text=era["text"], start_date=_parse_date(era["start_date"]), end_date=_parse_date(era["end_date"]), color=_parse_color(era.get("color")), side=_parse_side(era.get("side")), alpha=era.get("alpha", 0.3), ) # Era spans for span in d.get("era_spans", []): graph.add_era_span( text=span["text"], start_date=_parse_date(span["start_date"]), end_date=_parse_date(span["end_date"]), color=_parse_color(span.get("color")), hint=_parse_hint(span.get("hint")), side=_parse_side(span.get("side")), color_start_and_end_markers=span.get("color_start_and_end_markers", False), ) # Styling if apply_styling and "styling" in d: styling = d["styling"] if "x_axis" in styling: x = styling["x_axis"] graph.format_x_axis( text=x.get("text"), positionx=x.get("positionx"), positiony=x.get("positiony"), color=_parse_color(x.get("color")), fontsize=x.get("fontsize"), ) if "y_axis" in styling: y = styling["y_axis"] graph.format_y_axis( text=y.get("text"), positionx=y.get("positionx"), positiony=y.get("positiony"), color=_parse_color(y.get("color")), fontsize=y.get("fontsize"), ) return graph