Skip to content

Commit

Permalink
unstable commit; work in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
ndenny committed Nov 22, 2024
1 parent 90cd048 commit eb11ecb
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 47 deletions.
107 changes: 60 additions & 47 deletions lib/Bastion/CARP.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"""
import uuid

from Bastion.Common import toJDN


class ReplyStatus(tuple):
DEFAULT_CATEGORY_GLOSS = {
Expand Down Expand Up @@ -125,71 +127,63 @@ 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())

if context is not None:
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

Expand All @@ -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)
49 changes: 49 additions & 0 deletions lib/Bastion/Common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
38 changes: 38 additions & 0 deletions lib/Bastion/Packers/CARP.py
Original file line number Diff line number Diff line change
@@ -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'

0 comments on commit eb11ecb

Please sign in to comment.