From eb11ecb8c155ba1e9c4fc5f489905deeccd522bd Mon Sep 17 00:00:00 2001 From: Nathan Denny Date: Fri, 22 Nov 2024 13:43:15 -0500 Subject: [PATCH] unstable commit; work in progress --- lib/Bastion/CARP.py | 107 ++++++++++++++++++++---------------- lib/Bastion/Common.py | 49 +++++++++++++++++ lib/Bastion/Packers/CARP.py | 38 +++++++++++++ 3 files changed, 147 insertions(+), 47 deletions(-) create mode 100644 lib/Bastion/Packers/CARP.py diff --git a/lib/Bastion/CARP.py b/lib/Bastion/CARP.py index bc3bc62..6fda2ee 100644 --- a/lib/Bastion/CARP.py +++ b/lib/Bastion/CARP.py @@ -5,6 +5,8 @@ """ import uuid +from Bastion.Common import toJDN + class ReplyStatus(tuple): DEFAULT_CATEGORY_GLOSS = { @@ -125,37 +127,39 @@ def toJDN(self, **kwargs): jdn = { 'ID': self.ID, 'action': self.action, + 'args': { }, 'opened': self.when.isoformat(), 'context': { } } - if args: - jdn['args'] = list(args) + if self.args: + for k, v in self.args.items(): + jdn['args'][k] = toJDN(v) for k, v in self.context.items(): - jdn['context'][k] = v + jdn['context'][k] = toJDN(v) return jdn - class isReceipt: - def __init__(self, request, status, obj, context = None, **kwargs): - #-- request is a 2-tuple of (action, [arg1, ...]) - #-- status is an instance of ReplyStatus - #-- brief is an arbitrary (assumed human readable) text string - #-- obj is any JSON serializable object. - #-- context, if given, is a dict of extra key-value associations. - if not isinstance(request, isRequest): - raise ValueError("request must be of type isRequest") - - if not isinstance(status, ReplyStatus): - raise ValueError("status must be of type ReplyStatus") + def __init__(self, request, status, obj, **kwargs): + #-- POSITIONAL ARGS... + #-- request is a REQUIRED 2-tuple of (action, [arg1, ...]) + #-- status is a REQUIRED instance of ReplyStatus + #-- obj is a REQUIRED JSON serializable object, the outcome of the action implied by the request. + #-- KEYWORD ARGS... + #-- context: an OPTIONAL dict of extra key-value associations. + #-- ID: an OPTIONAL given ID string, defaults to a randomly generated UUID + #-- lede: a headline message for the result, defaults to the category of the reply status + #-- when: an OPTIONAL instance of datetime that indicates when the result was produced. + assert isinstance(request, isRequest), "request must be of type isRequest" + assert isinstance(status, ReplyStatus), "status must be of type ReplyStatus" self.ID = kwargs.get('ID', str(uuid.uuid4())) self.request = request self.status = status self.lede = kwargs.get('lede', status.category) - self.body = obj + self.record = obj self.context = { } self.when = kwargs.get('when', datetime.datetime.now()) @@ -163,33 +167,23 @@ def __init__(self, request, status, obj, context = None, **kwargs): self.context = context.copy() def toJDN(self, **kwargs): - #-- receipts have FOUR top level sections: request, result, context, and data. - #-- result is the http-like status code and message, e.g. (200, "Ok") - #-- brief is a single text string that is a human readable summary of the receipt. - #-- context is an arbitrary set of key-value pairs - #-- data is a JSON serializable object that is the body of the receipt. - if callable(getattr(obj, 'toJDN', None)): - payload = self.body.toJDN() - else: - payload = self.body - #-- Build result stanza. result = { 'ID': self.ID, 'status': self.status.code, 'message': self.status.gloss, 'lede': self.lede, - 'answered': self.when.isoformat(), + 'answered': self.when.isoformat( ), 'context': { }, - 'body': payload + 'record': toJDN(self.record) } #-- Populate the result's context (if any) - for k, v in self.context.items(): - result['context'][k] = v + for k, v in self.context.items( ): + result['context'][k] = toJDN(v) #-- Construct the JDN for return jdn = { } - jdn['request'] = self.request.toJDN(**kwargs) + jdn['request'] = toJDN(self.request) jdn['request']['result'] = result return jdn @@ -211,29 +205,48 @@ class Receipt(isReceipt): class Report(isReceipt): - def __init__(self, request, status, obj = None, **kwargs): - isReceipt.__init__(self, request, status, obj, **kwargs) - self.report = kwargs.get('report', None) + def __init__(self, request, status, *args, **kwargs): + isReceipt.__init__(self, request, status, *args, **kwargs) + self._body = kwargs.get('report', None) + + def changed(self, *args): + if 'body' in args: + self._body = None def toJDN(self, **kwargs): jdn = isReceipt.toJDN(self, **kwargs) jdn['request']['result']['report'] = str(self.report) return jdn + @property + def body(self): + if getattr(self, '_body', None) is None: + self._body = str(self) + return self._body + + class Request(isRequest): #-- Subclasses can set their own REPORT. #-- This is used mostly in the convenience methods: .succeeded, .failed, etc. - REPORT = Report - - def succeeded(self, doc, data = None, **kwargs): - return self.REPORT(self, ReplyStatus.Ok, doc, data, **kwargs) - - def failed(self, doc, data = None, **kwargs): - return self.REPORT(self, ReplyStatus.Failed, doc, data, **kwargs)) - - def crashed(self, doc, data = None, **kwargs): - return self.REPORT(self, ReplyStatus.Crashed, doc, data, **kwargs)) - - def inconclusive(self, doc, data = None, **kwargs) - return self.REPORT(self, ReplyStatus.Inconclusive, doc, data, **kwargs) + REPORTS = { } #-- An optional mapping of ReplyStatus code to report class or function. + + def succeeded(self, *args, **kwargs): + status = ReplyStatus.Ok + mkReport = self.REPORTS.get(status.code, Report) + return mkReport(self, status, *args, **kwargs) + + def failed(self, *args, **kwargs): + status = ReplyStatus.Failed + mkReport = self.REPORTS.get(status.code, Report) + return mkReport(self, status, *args, **kwargs) + + def crashed(self, *args, **kwargs): + status = ReplyStatus.Crashed + mkReport = self.REPORTS.get(status.code, Report) + return mkReport(self, status, *args, **kwargs) + + def inconclusive(self, *args, **kwargs) + status = ReplyStatus.Inconclusive + mkReport = self.REPORTS.get(status.code, Report) + return mkReport(self, status, *args, **kwargs) diff --git a/lib/Bastion/Common.py b/lib/Bastion/Common.py index a59c7ac..653d229 100644 --- a/lib/Bastion/Common.py +++ b/lib/Bastion/Common.py @@ -46,6 +46,55 @@ def RDN(x): raise ValueError("RDN(x) - requires that object x have an .RDN property") +class Alchemist(tuple): + """ + I am an extensible translator. + """ + ALCHEMISTS = { } + def __new__(cls, form, magic = None): + if form not in Alchemist.ALCHEMISTS: + if not magic: + magic = 'to{}'.format(form) + Alchemist.ALCHEMISTS[form] = tuple.__new__(cls, (form, magic)) + return Alchemist.ALCHEMISTS[form] + + def __init__(self, form, magic = None): + if getattr(self, 'spells', None) is None: + self.spells = [ ] + + form = property(operator.itemgetter(0)) + magic = property(operator.itemgetter(1)) + + def __call__(self, subject, **kwargs): + #-- first, we look to see if the given subject already has an embedded map (spell) + #-- e.g. if we're trying to cast the subject into a JSON serializable dictionary (JDN), + #-- then we'd look to see if subject as a .toJDN() method (or some such) + spell = getattr(subject, self.magic, None) + if spell: + if callable(spell): + return spell(**kwargs) + else: + return spell + + #-- If we get to here, then the subject doesn't know how to map itself, + #-- we'll go through our internal list of spells and see if anything matches. + for indicator, transmute in self.spells: + if isinstance(indicator, type): + recognize = lambda x: isinstance(x, indicator) + else: + recognize = indicator + if recognize(subject): + return transmute(subject, **kwargs) + + #-- Finally, if we've gotten all the way to here, we just answer with what we were given. + return subject + +toJDN = Alchemist('JDN') +def _toJDN_date(x, **kwargs): + #-- x is an instance of datetime.date + return x.isoformat() +toJDN.spells.append( (datetime.date, _toJDN_date) ) + class entity: """ diff --git a/lib/Bastion/Packers/CARP.py b/lib/Bastion/Packers/CARP.py new file mode 100644 index 0000000..eeeedb1 --- /dev/null +++ b/lib/Bastion/Packers/CARP.py @@ -0,0 +1,38 @@ +import logging + +from Bastion.Common import Thing, Unknown +import Bastion.Model +from Bastion.Curator import Manifest, BLONDE, Snap +from Bastion.CARP import * + + +logger = logging.getLogger(__name__) + + +class PackReportSuccess(Report): + def __init__(self, request, status, *args, **kwargs): + Report.__init__(self, request, status, *args, **kwargs) + + +class PackRequest(Request): + REPORTS = { + RequestStatus.Ok.code: PackReportSuccess + } + + def __init__(self, asset, basis = None, **kwargs): + #-- Which asset will be packed? + #-- If differential backup, what basis is used for determining changes? + Request.__init__(self, kwargs.get("action", "pack")) + + self.context['asset'] = str(asset.CURIE) + self.context['halo'] = str(asset.halo) + + if basis: + self.context['detail'] = 'D' + self.context['basis'] = str(kwargs['basis']) + self.context['whence'] = kwargs['whence'].isoformat() + if 'genus' in kwargs: + self.context['genus'] = kwargs['genus'] + else: + self.context['detail'] = 'F' +