Rows on disk: typed identity keys, lossless values, compressed JSON Lines.
A staged or folded row is one JSON object per line, in zstd frames. JSON keeps
an absent field apart from a null one. Values JSON cannot hold (dates, times,
UUIDs, decimals) are written as {"$graflo": <type>, "v": <text>} and read
back as the same Python value.
Attributes
TAG = '$graflo'
module-attribute
__all__ = ['TAG', 'IdentityKeyError', 'decode', 'dumps', 'encode', 'identity_member', 'key_member', 'read_rows', 'typed_key', 'write_rows']
module-attribute
Classes
IdentityKeyError
Bases: ValueError
An identity value that cannot key a row: null, empty, NaN or not a scalar.
Source code in graflo/util/typed_values.py
| class IdentityKeyError(ValueError):
"""An identity value that cannot key a row: null, empty, NaN or not a scalar."""
|
Functions:
decode(value)
The inverse of :func:encode.
Source code in graflo/util/typed_values.py
| def decode(value: Any) -> Any:
"""The inverse of :func:`encode`."""
if isinstance(value, dict):
if len(value) == 2 and TAG in value and "v" in value:
return _DECODERS[value[TAG]](value["v"])
return {k: decode(v) for k, v in value.items()}
if isinstance(value, list):
return [decode(v) for v in value]
return value
|
dumps(row)
One row as a JSON line.
Source code in graflo/hq/bulk/encode.py
| def dumps(row: dict[str, Any]) -> bytes:
"""One row as a JSON line."""
return (
json.dumps(row, separators=(",", ":"), ensure_ascii=False, allow_nan=False)
+ "\n"
).encode()
|
encode(value)
value as JSON-ready data, non-JSON scalars tagged.
Raises:
| Type |
Description |
TypeError
|
A value graflo does not write.
|
Source code in graflo/util/typed_values.py
| def encode(value: Any) -> Any:
"""*value* as JSON-ready data, non-JSON scalars tagged.
Raises:
TypeError: A value graflo does not write.
"""
if value is None or isinstance(value, bool | int | str):
return value
if isinstance(value, float):
return value if math.isfinite(value) else {TAG: "float", "v": repr(value)}
if isinstance(value, dict):
return {str(k): encode(v) for k, v in value.items()}
if isinstance(value, list | tuple):
return [encode(v) for v in value]
if isinstance(value, dt.datetime):
return {TAG: "datetime", "v": value.isoformat()}
if isinstance(value, dt.date):
return {TAG: "date", "v": value.isoformat()}
if isinstance(value, dt.time):
return {TAG: "time", "v": value.isoformat()}
if isinstance(value, UUID):
return {TAG: "uuid", "v": str(value)}
if isinstance(value, Decimal):
return {TAG: "decimal", "v": str(value)}
raise TypeError(f"cannot stage a value of type {type(value).__name__}")
|
identity_member(value)
An edge identity field's value as part of the edge key; absent is null.
A value that cannot key a vertex keys an edge by its encoded form.
Source code in graflo/util/typed_values.py
| def identity_member(value: Any) -> Any:
"""An edge identity field's value as part of the edge key; absent is null.
A value that cannot key a vertex keys an edge by its encoded form.
"""
if value is None:
return None
try:
return key_member(value)
except IdentityKeyError:
return encode(value)
|
key_member(value)
One identity value as [kind, value], compared as the targets compare it.
Numbers compare by value (1 and 1.0 are one key), booleans apart
from numbers, strings apart from numbers (1 and "1" are two keys).
A UUID keys as its text, the way every backend stores it.
Raises:
Source code in graflo/util/typed_values.py
| def key_member(value: Any) -> list[Any]:
"""One identity value as ``[kind, value]``, compared as the targets compare it.
Numbers compare by value (``1`` and ``1.0`` are one key), booleans apart
from numbers, strings apart from numbers (``1`` and ``"1"`` are two keys).
A UUID keys as its text, the way every backend stores it.
Raises:
IdentityKeyError: The value cannot key a row.
"""
if value is None or value == "":
raise IdentityKeyError("empty identity value")
if isinstance(value, bool):
return ["b", value]
if isinstance(value, int):
return ["n", value]
if isinstance(value, float | Decimal):
if isinstance(value, float) and not math.isfinite(value):
raise IdentityKeyError(f"identity value {value!r} is not finite")
if value == int(value):
return ["n", int(value)]
return ["n", float(value)]
if isinstance(value, str):
return ["s", value]
if isinstance(value, UUID):
return ["s", str(value)]
if isinstance(value, dt.datetime):
return ["t", value.isoformat()]
if isinstance(value, dt.date):
return ["d", value.isoformat()]
if isinstance(value, dt.time):
return ["h", value.isoformat()]
raise IdentityKeyError(
f"identity value of type {type(value).__name__} is not a scalar"
)
|
read_rows(path)
The rows of a zstd JSON Lines file, values decoded.
Source code in graflo/hq/bulk/encode.py
| def read_rows(path: Path) -> Iterator[dict[str, Any]]:
"""The rows of a zstd JSON Lines file, values decoded."""
zstd = _deps.zstandard()
with open(path, "rb") as raw:
reader = zstd.ZstdDecompressor().stream_reader(raw, read_across_frames=True)
buffer = b""
while chunk := reader.read(1 << 20):
buffer += chunk
*lines, buffer = buffer.split(b"\n")
for line in lines:
if line:
yield decode(json.loads(line))
if buffer.strip():
yield decode(json.loads(buffer))
|
typed_key(values)
The canonical text of a tuple of identity values; equal keys, equal text.
A composite identity may lack members, as an upsert matches them: an
absent one keys as null, an empty string as itself.
Raises:
| Type |
Description |
IdentityKeyError
|
No member has a value, or one cannot key a row.
|
Source code in graflo/util/typed_values.py
| def typed_key(values: Iterable[Any]) -> str:
"""The canonical text of a tuple of identity values; equal keys, equal text.
A composite identity may lack members, as an upsert matches them: an
absent one keys as null, an empty string as itself.
Raises:
IdentityKeyError: No member has a value, or one cannot key a row.
"""
values = list(values)
if all(v is None or v == "" for v in values):
raise IdentityKeyError("no identity value")
members = [
None if v is None else ["s", ""] if v == "" else key_member(v) for v in values
]
return json.dumps(members, separators=(",", ":"), ensure_ascii=False)
|
write_rows(path, rows)
Write JSON lines to path as one zstd frame, atomically; return the count.
The file appears complete or not at all, so a crash never leaves a part.
Source code in graflo/hq/bulk/encode.py
| def write_rows(path: Path, rows: Iterable[bytes]) -> int:
"""Write JSON lines to *path* as one zstd frame, atomically; return the count.
The file appears complete or not at all, so a crash never leaves a part.
"""
zstd = _deps.zstandard()
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_name(path.name + ".tmp")
count = 0
with open(tmp, "wb") as raw:
with zstd.ZstdCompressor().stream_writer(raw, closefd=False) as out:
for line in rows:
out.write(line)
count += 1
raw.flush()
os.fsync(raw.fileno())
os.replace(tmp, path)
return count
|