Source code for pykern.quest
"""API wrapper
:copyright: Copyright (c) 2024 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
from pykern.pkcollections import PKDict
from pykern.pkdebug import pkdc, pkdlog, pkdp, pkdformat
import contextlib
[docs]
class API(PKDict):
"""Holds request context for all API calls."""
METHOD_PREFIX = "api_"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._destroyed = False
[docs]
def is_quest_end(self):
return self._destroyed
[docs]
def quest_end(self, in_error=True):
def _attr_end(name, attr):
try:
attr.quest_end(self, in_error=in_error)
except Exception:
pkdlog("destroy failed {}={} stack={}", name, attr, pkdexc())
if self._destroyed:
return
x = reversed(list(self.__attrs()))
self.clear()
# must be after the clear
self._destroyed = True
for k, v in x:
_attr_end(k, v)
[docs]
def quest_init(self, attr_classes, init_kwargs):
def _set(name, attr):
if not isinstance(attr, Attr):
raise AssertionError(f"type=type(obj) not Attr name={name}")
if name in self:
raise AssertionError(f"name={name} already added")
self[name] = attr
for a in attr_classes:
s = a if isinstance(a, Attr) else a.quest_init(self, init_kwargs)
_set(s.ATTR_KEY, s)
[docs]
def quest_start(self):
for _, v in self.__attrs():
v.quest_start(self)
def __attrs(self):
for k, v in self.items():
if isinstance(k, Attr):
yield k, v
[docs]
class Attr(PKDict):
#: shared Attrs do not have link to qcall
IS_SINGLETON = False
def __init__(self, qcall, **kwargs):
"""Initialize object
Subclasses must define ATTR_KEY so it can be added to qcall.
If `IS_SINGLETON` is true then qcall must be None. This will
only be called outside of quest_init. Otherwise, qcall is
bound to instance.
Args:
qcall (API): what qcall is being initialized
kwargs (dict): inserted into dictionary
"""
if self.IS_SINGLETON:
assert qcall is None
super().__init__(**kwargs)
else:
# It may be qcall is None at this point, but that's ok. Will be set below
super().__init__(qcall=qcall, **kwargs)
[docs]
def quest_end(self, qcall, in_error):
"""Called when quest ends
Right before destroy. No other attributes are available.
Args:
qcall (API): qcall being ended
in_error (bool): True, aborting quest. False, successful quest [True]
"""
pass
[docs]
@classmethod
def quest_init(cls, qcall, init_kwargs):
"""Initialize an instance of cls and put on qcall
If `IS_SINGLETON`, qcall is not put on self. `kwargs` must contain ATTR_KEY,
which is an instance of class.
Args:
qcall (API): quest being initialized
init_kwargs (PKDict): values to passed to `start`
Returns:
Attr: instance to bind to quest
"""
if not cls.IS_SINGLETON:
self = cls(qcall, **init_kwargs)
elif (self := init_kwargs.get(cls.ATTR_KEY)) is None:
raise AssertionError(
f"init_kwargs does not contain singleton key={cls.ATTR_KEY}"
)
if not isinstance(self, Attr):
raise AssertionError(f"{cls.ATTR_KEY}={self} not instance of Attr")
return self
[docs]
def quest_start(self, qcall):
"""Called after all attrs are initialized
Args:
qcall (API): quest being started
"""
pass
[docs]
class Spec:
# qspec
pass
[docs]
@contextlib.contextmanager
def start(api_class, attr_classes, **kwargs):
qcall = api_class()
e = True
try:
qcall.quest_init(attr_classes, PKDict(kwargs))
qcall.quest_start()
yield qcall
e = False
finally:
qcall.quest_end(in_error=e)