turbo_broccoli.turbo_broccoli

Main module containing the JSON encoder and decoder methods.

  1"""Main module containing the JSON encoder and decoder methods."""
  2
  3import json
  4import zlib
  5from pathlib import Path
  6from typing import Any
  7
  8from turbo_broccoli import user
  9from turbo_broccoli.context import Context
 10from turbo_broccoli.custom import get_decoders, get_encoders
 11from turbo_broccoli.exceptions import TypeIsNodecode, TypeNotSupported
 12
 13
 14def _from_jsonable(obj: Any, ctx: Context) -> Any:
 15    """
 16    Takes an object fresh from `json.load` or `json.loads` and loads types that
 17    are supported by TurboBroccoli therein.
 18    """
 19    if isinstance(obj, dict):
 20        obj = {k: _from_jsonable(v, ctx / k) for k, v in obj.items()}
 21        if "__type__" in obj:
 22            try:
 23                ctx.raise_if_nodecode(obj["__type__"])
 24                base = obj["__type__"].split(".")[0]
 25                if base == "user":
 26                    name = ".".join(obj["__type__"].split(".")[1:])
 27                    if decoder := user.decoders.get(name):
 28                        obj = decoder(obj, ctx)
 29                else:
 30                    obj = get_decoders()[base](obj, ctx)
 31            except TypeIsNodecode:
 32                pass
 33    elif isinstance(obj, list):
 34        return [_from_jsonable(v, ctx / str(i)) for i, v in enumerate(obj)]
 35    elif isinstance(obj, tuple):
 36        return tuple(
 37            _from_jsonable(v, ctx / str(i)) for i, v in enumerate(obj)
 38        )
 39    return obj
 40
 41
 42def _make_or_set_ctx(
 43    file_path: str | Path | None, ctx: Context | None, **kwargs
 44) -> Context:
 45    """
 46    Generate a context object that is consistent with the inputs.
 47    """
 48    if file_path is None and ctx is None:
 49        raise ValueError(
 50            "Either a file path or a context (or both) must be provided."
 51        )
 52    if file_path is not None and ctx is not None:
 53        if ctx.file_path is not None and ctx.file_path != file_path:
 54            raise ValueError(
 55                "The file path in the context does not match the provided "
 56                "file path."
 57            )
 58        assert isinstance(file_path, (str, Path))  # for typechecking
 59        ctx.file_path = Path(file_path)
 60    if ctx is None:
 61        ctx = Context(file_path=file_path, **kwargs)
 62    return ctx
 63
 64
 65def _to_jsonable(obj: Any, ctx: Context) -> Any:
 66    """
 67    Transforms an object (dict, list, primitive) that possibly contains types
 68    that TurboBroccoli's custom encoders support, and returns an object that is
 69    readily vanilla JSON-serializable.
 70    """
 71    name = obj.__class__.__name__
 72    if name in user.encoders:
 73        obj = user.encoders[name](obj, ctx)
 74    for encoder in get_encoders():
 75        try:
 76            obj = encoder(obj, ctx)
 77            break
 78        except TypeNotSupported:
 79            pass
 80    if isinstance(obj, dict):
 81        return {k: _to_jsonable(v, ctx / k) for k, v in obj.items()}
 82    if isinstance(obj, list):
 83        return [_to_jsonable(v, ctx / str(i)) for i, v in enumerate(obj)]
 84    if isinstance(obj, tuple):
 85        return tuple(_to_jsonable(v, ctx / str(i)) for i, v in enumerate(obj))
 86    return obj
 87
 88
 89def from_json(doc: str, ctx: Context | None = None) -> Any:
 90    """
 91    Deserializes a JSON string. The context's file path and compression setting
 92    will be ignored.
 93    """
 94    return _from_jsonable(json.loads(doc), Context() if ctx is None else ctx)
 95
 96
 97def load_json(
 98    file_path: str | Path | None = None, ctx: Context | None = None, **kwargs
 99) -> Any:
100    """
101    Loads a JSON file.
102
103    Args:
104        file_path (str | Path | None): If left to `None`, a context with a file
105            path must be provided
106        ctx (Context | None): The context to use. If `None`, a new context will
107            be created with the kwargs.
108        **kwargs: Forwarded to the `turbo_broccoli.context.Context`
109            constructor. If `ctx` is provided, the kwargs are ignored.
110    """
111    ctx = _make_or_set_ctx(file_path, ctx, **kwargs)
112    assert isinstance(ctx.file_path, Path)  # for typechecking
113    if ctx.compress:
114        with ctx.file_path.open(mode="rb") as fp:
115            s = zlib.decompress(fp.read()).decode("utf-8")
116        return _from_jsonable(json.loads(s), ctx)
117    with ctx.file_path.open(mode="r", encoding="utf-8") as fp:
118        return _from_jsonable(json.load(fp), ctx)
119
120
121def save_json(
122    obj: Any,
123    file_path: str | Path | None = None,
124    ctx: Context | None = None,
125    **kwargs,
126) -> None:
127    """
128    Serializes an object and writes the result to a file. The artifact path and
129    the output file's parent folder will be created if they don't exist.
130
131    Args:
132        obj (Any):
133        file_path (str | Path):
134        ctx (Context | None): The context to use. If `None`, a new context will
135            be created with the kwargs.
136        **kwargs: Forwarded to the `turbo_broccoli.context.Context`
137            constructor.
138    """
139    ctx = _make_or_set_ctx(file_path, ctx, **kwargs)
140    data = json.dumps(_to_jsonable(obj, ctx))
141    assert isinstance(ctx.file_path, Path)  # for typechecking
142    if not ctx.file_path.parent.exists():
143        ctx.file_path.parent.mkdir(parents=True)
144    if ctx.compress:
145        with ctx.file_path.open(mode="wb") as fp:
146            fp.write(zlib.compress(data.encode("utf-8")))
147    else:
148        with ctx.file_path.open(mode="w", encoding="utf-8") as fp:
149            fp.write(data)
150
151
152def to_json(obj: Any, ctx: Context | None = None) -> str:
153    """
154    Converts an object to a JSON string. The context's artifact folder will be
155    created if it doesn't exist. The context's file path and compression
156    setting will be ignored.
157    """
158    ctx = Context() if ctx is None else ctx
159    if not ctx.artifact_path.exists():
160        ctx.artifact_path.mkdir(parents=True)
161    return json.dumps(_to_jsonable(obj, ctx))
def from_json(doc: str, ctx: turbo_broccoli.context.Context | None = None) -> Any:
90def from_json(doc: str, ctx: Context | None = None) -> Any:
91    """
92    Deserializes a JSON string. The context's file path and compression setting
93    will be ignored.
94    """
95    return _from_jsonable(json.loads(doc), Context() if ctx is None else ctx)

Deserializes a JSON string. The context's file path and compression setting will be ignored.

def load_json( file_path: str | pathlib.Path | None = None, ctx: turbo_broccoli.context.Context | None = None, **kwargs) -> Any:
 98def load_json(
 99    file_path: str | Path | None = None, ctx: Context | None = None, **kwargs
100) -> Any:
101    """
102    Loads a JSON file.
103
104    Args:
105        file_path (str | Path | None): If left to `None`, a context with a file
106            path must be provided
107        ctx (Context | None): The context to use. If `None`, a new context will
108            be created with the kwargs.
109        **kwargs: Forwarded to the `turbo_broccoli.context.Context`
110            constructor. If `ctx` is provided, the kwargs are ignored.
111    """
112    ctx = _make_or_set_ctx(file_path, ctx, **kwargs)
113    assert isinstance(ctx.file_path, Path)  # for typechecking
114    if ctx.compress:
115        with ctx.file_path.open(mode="rb") as fp:
116            s = zlib.decompress(fp.read()).decode("utf-8")
117        return _from_jsonable(json.loads(s), ctx)
118    with ctx.file_path.open(mode="r", encoding="utf-8") as fp:
119        return _from_jsonable(json.load(fp), ctx)

Loads a JSON file.

Args: file_path (str | Path | None): If left to None, a context with a file path must be provided ctx (Context | None): The context to use. If None, a new context will be created with the kwargs. **kwargs: Forwarded to the turbo_broccoli.context.Context constructor. If ctx is provided, the kwargs are ignored.

def save_json( obj: Any, file_path: str | pathlib.Path | None = None, ctx: turbo_broccoli.context.Context | None = None, **kwargs) -> None:
122def save_json(
123    obj: Any,
124    file_path: str | Path | None = None,
125    ctx: Context | None = None,
126    **kwargs,
127) -> None:
128    """
129    Serializes an object and writes the result to a file. The artifact path and
130    the output file's parent folder will be created if they don't exist.
131
132    Args:
133        obj (Any):
134        file_path (str | Path):
135        ctx (Context | None): The context to use. If `None`, a new context will
136            be created with the kwargs.
137        **kwargs: Forwarded to the `turbo_broccoli.context.Context`
138            constructor.
139    """
140    ctx = _make_or_set_ctx(file_path, ctx, **kwargs)
141    data = json.dumps(_to_jsonable(obj, ctx))
142    assert isinstance(ctx.file_path, Path)  # for typechecking
143    if not ctx.file_path.parent.exists():
144        ctx.file_path.parent.mkdir(parents=True)
145    if ctx.compress:
146        with ctx.file_path.open(mode="wb") as fp:
147            fp.write(zlib.compress(data.encode("utf-8")))
148    else:
149        with ctx.file_path.open(mode="w", encoding="utf-8") as fp:
150            fp.write(data)

Serializes an object and writes the result to a file. The artifact path and the output file's parent folder will be created if they don't exist.

Args: obj (Any): file_path (str | Path): ctx (Context | None): The context to use. If None, a new context will be created with the kwargs. **kwargs: Forwarded to the turbo_broccoli.context.Context constructor.

def to_json(obj: Any, ctx: turbo_broccoli.context.Context | None = None) -> str:
153def to_json(obj: Any, ctx: Context | None = None) -> str:
154    """
155    Converts an object to a JSON string. The context's artifact folder will be
156    created if it doesn't exist. The context's file path and compression
157    setting will be ignored.
158    """
159    ctx = Context() if ctx is None else ctx
160    if not ctx.artifact_path.exists():
161        ctx.artifact_path.mkdir(parents=True)
162    return json.dumps(_to_jsonable(obj, ctx))

Converts an object to a JSON string. The context's artifact folder will be created if it doesn't exist. The context's file path and compression setting will be ignored.