# -*- encoding: utf-8 -*-
"""
keri.app.configing module
"""
import json
import os
import cbor2 as cbor
import hjson
import msgpack
from hio.base import filing, doing
from ..help import ogler
logger = ogler.getLogger()
[docs]
def openCF(cls=None, filed=True, **kwa):
"""Return a context manager that opens a Configer instance.
Thin wrapper around :func:`filing.openFiler` with ``Configer`` as the
default class and ``filed=True`` as the default.
Args:
cls (type, optional): Filer subclass to instantiate. Defaults to
``Configer`` when ``None``.
filed (bool): ``True`` means ``.path`` is a file path rather than a
directory path. Defaults to ``True``.
**kwa: Additional keyword arguments forwarded to
:func:`filing.openFiler`.
Returns:
contextlib.AbstractContextManager: Context manager that yields an open
``Configer`` instance.
"""
if cls == None: # can't reference class before its defined below
cls = Configer
return filing.openFiler(cls=cls, filed=filed, **kwa)
[docs]
class Configer(filing.Filer):
"""Habitat config file reader/writer.
Extends :class:`filing.Filer` to support reading and writing a single
config file in one of four serialization formats determined by the file
extension:
- ``.json`` — HJSON (human-friendly superset of JSON) when ``human=True``;
strict JSON with two-space indentation when ``human=False``.
- ``.mgpk`` — MsgPack binary.
- ``.cbor`` — CBOR binary.
Attributes:
human (bool): When ``True`` and the file extension is ``.json``, use
HJSON for serialization. When ``False``, use strict JSON.
Example:
Expected JSON/HJSON config file structure:
.. code-block:: json
{
"dt": "2021-01-01T00:00:00.000000+00:00",
"nel":
{
"dt": "2021-01-01T00:00:00.000000+00:00",
"curls":
[
"tcp://localhost:5621/"
]
},
"iurls":
[
"tcp://localhost:5620/?role=peer&name=tam"
],
"durls":
[
"http://127.0.0.1:7723/oobi/EB...",
"http://127.0.0.1:7723/oobi/EM..."
],
"wurls":
[
"http://127.0.0.1:5644/.well-known/keri/oobi/EB..."
]
}
"""
TailDirPath = os.path.join("keri", "cf")
CleanTailDirPath = os.path.join("keri", "clean", "cf")
AltTailDirPath = os.path.join(".keri", "cf")
AltCleanTailDirPath = os.path.join(".keri", "clean", "cf")
TempPrefix = "keri_cf_"
[docs]
def __init__(self, name="conf", base="main", filed=True, mode="r+b",
fext="json", human=True, **kwa):
"""Initialize and open the config file.
Args:
name (str): Leaf name component used to differentiate multiple
KERI installations on the same host. Defaults to ``"conf"``.
base (str): Optional intermediate directory segment inserted
between the head directory and ``name``. An empty string
omits this segment. Defaults to ``"main"``.
filed (bool): ``True`` means ``.path`` is a file path; ``False``
means ``.path`` is a directory path. Defaults to ``True``.
mode (str): File open mode. Defaults to ``"r+b"`` (read/write
binary without truncation).
fext (str): File extension (without leading dot) used when
``filed=True``. Determines the serialization format.
Defaults to ``"json"``.
human (bool): When ``True`` and ``fext`` is ``"json"``, use HJSON
for :meth:`put` and :meth:`get`. Defaults to ``True``.
**kwa: Additional keyword arguments forwarded to
:class:`filing.Filer`.
"""
super(Configer, self).__init__(name=name,
base=base,
filed=filed,
mode=mode,
fext=fext,
**kwa)
self.human = True if human else False
[docs]
def put(self, data: dict, human=None):
"""Serialize ``data`` and overwrite the config file.
Truncates the file before writing. The serialization format is
determined by the file extension of ``.path``:
- ``.json`` — HJSON when ``human`` is truthy, strict JSON otherwise.
- ``.mgpk`` — MsgPack.
- ``.cbor`` — CBOR.
Args:
data (dict): Data to serialize and write.
human (bool, optional): Override ``self.human`` for this call.
``None`` means use ``self.human``. Defaults to ``None``.
Returns:
bool: ``True`` on success.
Raises:
ValueError: If the file is not open.
IOError: If the file extension is not ``.json``, ``.mgpk``, or
``.cbor``.
"""
if not self.file or self.file.closed:
raise ValueError(f"File '{self.path}' not opened.")
human = human if human is not None else self.human
self.file.seek(0)
self.file.truncate()
root, ext = os.path.splitext(self.path)
if ext == '.json': # json can't dump to binary
if human:
ser = hjson.dumps(data)
else:
ser = json.dumps(data, indent=2)
ser = ser.encode("utf-8")
elif ext == '.mgpk':
ser = msgpack.dumps(data)
elif ext == '.cbor':
ser = cbor.dumps(data)
else:
raise IOError(f"Invalid file path ext '{ext}' "
f"not '.json', '.mgpk', or 'cbor'.")
self.file.write(ser)
self.file.flush()
os.fsync(self.file.fileno())
return True
[docs]
def get(self, human=None):
"""Read and deserialize the config file.
The deserialization format is determined by the file extension of
``.path``:
- ``.json`` — HJSON when ``human`` is truthy, strict JSON otherwise.
- ``.mgpk`` — MsgPack.
- ``.cbor`` — CBOR.
An empty file returns an empty dict without error.
Args:
human (bool, optional): Override ``self.human`` for this call.
``None`` means use ``self.human``. Defaults to ``None``.
Returns:
dict: Deserialized config data, or an empty dict if the file is
empty.
Raises:
ValueError: If the file is not open.
IOError: If the file extension is not ``.json``, ``.mgpk``, or
``.cbor``.
"""
if not self.file or self.file.closed:
raise ValueError(f"File '{self.path}' not opened.")
human = human if human is not None else self.human
it = {}
self.file.seek(0)
ser = self.file.read()
if ser: # not empty
root, ext = os.path.splitext(self.path)
if ext == '.json': # json.load works with bytes as well as str
if human:
it = hjson.loads(ser.decode("utf-8"))
else:
it = json.loads(ser.decode("utf-8"))
elif ext == '.mgpk':
it = msgpack.loads(ser)
elif ext == '.cbor':
it = cbor.loads(ser)
else:
raise IOError(f"Invalid file path ext '{ext}' "
f"not '.json', '.mgpk', or 'cbor'.")
return it
[docs]
class ConfigerDoer(doing.Doer):
"""Doer that manages the lifecycle of a :class:`Configer` instance.
Opens the ``Configer`` on :meth:`enter` if it is not already open, and
closes it on :meth:`exit`, clearing the underlying file if the
``Configer`` was opened in temporary mode.
Attributes:
done (bool): Completion flag. ``True`` when the doer has finished
normally; ``False`` when it exited early due to close or abort.
configer (Configer): The managed ``Configer`` instance.
Properties:
tyme (float): Relative cycle time obtained from the injected
``tymth`` closure.
tymth (callable): Closure returned by ``Tymist.tymeth()``. Calling
it returns the associated ``Tymist.tyme`` value.
tock (float): Desired interval in seconds between runs. Zero means
run as soon as possible. Must be non-negative.
"""
[docs]
def __init__(self, configer, **kwa):
"""Initialize the doer with a ``Configer`` instance.
Args:
configer (Configer): The ``Configer`` instance to manage.
**kwa: Additional keyword arguments forwarded to
:class:`doing.Doer`.
"""
super(ConfigerDoer, self).__init__(**kwa)
self.configer = configer
[docs]
def enter(self, *, temp=None):
"""Open the ``Configer`` if it is not already open.
Args:
temp (bool, optional): Unused. Present for interface compatibility
with the base ``Doer`` lifecycle. Defaults to ``None``.
"""
if not self.configer.opened:
self.configer.reopen()
[docs]
def exit(self):
"""Close the ``Configer``, clearing its file if opened in temp mode."""
self.configer.close(clear=self.configer.temp)