From 3eb5644fbe95ee07b1df672095265918140f80ff Mon Sep 17 00:00:00 2001 From: Nathan Denny Date: Fri, 17 Jan 2025 13:18:54 -0500 Subject: [PATCH] WORK IN PROGRESS --- lib/Bastion/CARP.py | 62 +++++++++++++++++++------------ lib/Bastion/Chronology.py | 1 + lib/Bastion/Clerks/BFD.py | 5 +-- lib/Bastion/Common.py | 71 ++++++++++++++++++++++++++++++++---- lib/Bastion/Curator.py | 5 ++- lib/Bastion/Movers/BFD.py | 3 +- lib/Bastion/Packers/TARs.py | 6 +-- lib/Bastion/Vaults/CARP.py | 7 ++-- lib/Bastion/Vaults/Common.py | 16 +++++--- 9 files changed, 128 insertions(+), 48 deletions(-) diff --git a/lib/Bastion/CARP.py b/lib/Bastion/CARP.py index 775f54f..5a47889 100644 --- a/lib/Bastion/CARP.py +++ b/lib/Bastion/CARP.py @@ -28,7 +28,8 @@ from collections.abc import Sequence -from Bastion.Common import toJDN +from Bastion.Common import toJDN, isBoxedObject, isTagged + class ReplyStatus(tuple): DEFAULT_CATEGORY_GLOSS = { @@ -258,12 +259,17 @@ def inconclusive(self): class isReport(isResult): def __init__(self, request, status, record = None, *args, **kwargs): isResult.__init__(self, request, status, record, *args, **kwargs) - self._body = kwargs.get('report', None) + #-- Can directy specify the human readable expression with "report =" + self._body = kwargs.get('report', None) + #-- Can delegate construction of human readable expression with "humanize = " + self._humanize = kwargs.get('humanize', None) @property def body(self): if getattr(self, '_body', None) is None: - if callable(getattr(self.record, 'toJDN', None)): + if callable(self._humanize): + self._body = self._humanize(self) + elif callable(getattr(self.record, 'toJDN', None)): self._body = pprint.pformat(self.record.toJDN()) else: self._body = pprint.pformat(self.record) @@ -330,41 +336,51 @@ class isReceipt: Mostly, I'm used for run-time type checking. """ def toJDN(self, **kwargs): - raise NotImplementedError("is subclass responsibility") + raise NotImplementedError(".toJDN is subclass responsibility") -class isReceiptOfResults(isReceipt): +class isWorkflowReceipt(isReceipt, isTagged): """ I am a thin wrapper around a sequence of results. - I am mostly used to build a single "record" from a sequence of operations, - with each operation having its own report. - Results can also be given with a key as part of a tuple such as (stage, result) where stage is a string key. + I am mostly used to build a single "record" from a sequence of actions with each action having its own result. + Results can also be given with an optional key as part of a tuple such as (stage, result) where stage is a string key. + isWorkflowReceipt(result1, result2, ...) + isWorkflowReceipt([('planning-stage', result1), ('running-stage', result2), ...]) """ - def __init__(self, results): + def __init__(self, *args): self.results = [ ] - self.tags = { } - for result in results: - if isinstance(result, isResult): - self.results.append(result) - elif isinstance(result, Sequence) and (len(result) == 2) and isinstance(result[1], isResult): - tag = str(result[0]) - result_actual = result[1] - self.results.append(result_actual) - self.tags[tag] = len(self.results) - else: - raise ValueError("result must be an instance of Bastion.CARP.isResult") + self._tags = { } + for result in args: + self.append(result) + + def append(self, result, *args): + if isinstance(result, isResult): + self.results.append(result) + i = len(self.results) + for arg in args: + self._tags[str(arg)] = i + elif isinstance(result, isBoxedObject): + self.results.append(result.actual) + i = len(self.results) + for tag in result.tags: + self._tags[tag] = i + else: + raise ValueError("result must be an instance of Bastion.CARP.isResult") + + @property + def tags(self): + return frozenset(self._tags.keys()) def __getitem__(self, n): if isinstance(n, int): return self.results[n] if isinstance(n, str): - i = self.keyed[n] + i = self._tags[n] return self.results[i] raise KeyError(n) def __iter__(self): - staged = [self.results[i] for i in range(len(self.results))] - return iter(staged) + return iter(self.results) def toJDN(self, **kwargs): jdn = { } diff --git a/lib/Bastion/Chronology.py b/lib/Bastion/Chronology.py index 37c455c..8aed592 100644 --- a/lib/Bastion/Chronology.py +++ b/lib/Bastion/Chronology.py @@ -162,6 +162,7 @@ def earliest(self): elapsed_seconds = (self.qM * Quantim.QUANTIM) * SECONDS return (y + elapsed_seconds) + @property def latest(self): """ answers a python datetime.datetime object that is the latest time possible within the range of this quantum. diff --git a/lib/Bastion/Clerks/BFD.py b/lib/Bastion/Clerks/BFD.py index 6f00cce..fbc5c2e 100644 --- a/lib/Bastion/Clerks/BFD.py +++ b/lib/Bastion/Clerks/BFD.py @@ -10,13 +10,12 @@ class Clerk(isClerk): def __init__(self, vault, root = None, **kwargs): - isClerk.__init__(self) + isClerk.__init__(self, vault) assert isinstance(vault, isVault), "vault must be an instance of Bastion.Model.isVault" if root: assert isinstance(root, pathlib.PosixPath), "root must be an instance of PosixPath" - self.vault = vault self.root = root if root is not None else vault.bank #----------------------------------------- @@ -76,7 +75,7 @@ def manifest(self, *args): else: raise ValueError - cell = self.bank / ark.site / ark.zone / ark.asset + cell = self.root / ark.site / ark.zone / ark.asset if cell.exists(): blondes = [ ] for item in cell.iterdir(): diff --git a/lib/Bastion/Common.py b/lib/Bastion/Common.py index 8cd01af..87ad283 100644 --- a/lib/Bastion/Common.py +++ b/lib/Bastion/Common.py @@ -13,6 +13,8 @@ import logging import urllib +from typing import Any, FrozenSet + import yaml @@ -114,33 +116,86 @@ def _toJDN_path(x, **kwargs): toJDN.magic.append( (Exception, _toJDN_Exception) ) toJDN.magic.append( (pathlib.PurePath, _toJDN_path) ) +class isTagged: + @property + def tags(self): + raise NotImplementedError(".tags -> FrozenSet[str] is subclass responsibility") + + @property + def hasTags(self): + return (len(self.tags) > 0) + -class entity: +class isBoxedObject(tuple, isTagged): """ I am syntactic sugar. Mostly, I do type checking, e.g. x = 4 if entity(x).isNumeric: print("it's a number!") """ - def __init__(self, subject): - self.subject = subject + def __new__(cls, obj, *args): + return tuple.__new__(cls, [obj] + [str(arg) for arg in args]) + + value = property(operator.itemgetter(0)) + actual = value #-- alias for "value" accessor + + def __repr__(self): + if not self.hasTags: + return "|{}|".format(repr(self.actual)) + else: + hashtags = ['#{}'.format(tag) for tag in sorted(self.tags)] + label = ",".join(hashtags) + return "|{}|~[{}]".format(repr(self.actual), label) + + @property + def tags(self): + if getattr(self, '_tags', None) is None: + self._tags = frozenset(self[1:]) + return self._tags @property def isNumeric(self): - return isinstance(self.subject, pynumbers.Real) + return isinstance(self.value, pynumbers.Real) @property def isDuration(self): - return isinstance(self.subject, datetime.timedelta) + return isinstance(self.value, datetime.timedelta) @property def isString(self): - return isinstance(self.subject, str) + return isinstance(self.value, str) @property def hasRDN(self): - return hasattr(self.subject, 'RDN') + return hasattr(self.value, 'RDN') + + @property + def hasTags(self): + return (len(self) > 1) + + def __call__(self): + return self.value + + def tagged(self, *args): + return self.__class__(self.value, *args) + + +def entity(obj): + """ + Shorthand for creating BoxedObject instances. + e.g. entity(4).isNumeric + e.g. entity(4, "low", "number") + e.g. entity(4).tagged("low", "number") + """ + return isBoxedObject(obj) + +def tag(obj, *args): + """ + Shorthand for creating BoxedObject instances. + This particular function signals that the primary purpose of the box is to associate tags. + """ + return isBoxedObject(obj, *args) class UnknownType(object): @@ -386,7 +441,7 @@ def prefer(options, **kwargs): return choices[0] return choices[:n] - + def asLogLevel(x): diff --git a/lib/Bastion/Curator.py b/lib/Bastion/Curator.py index ec51ced..bcf6f36 100644 --- a/lib/Bastion/Curator.py +++ b/lib/Bastion/Curator.py @@ -204,7 +204,7 @@ def anchor(self, item): .anchor(item:Snap) """ if isinstance(item, BLONDE): - return self._anchors[item.anchor] + return self._anchors[item.genus] if isinstance(item, str): return self.anchor( BLONDE.decode(item) ) if isinstance(item, Snap): @@ -237,6 +237,9 @@ def __init__(self, head, anchor): self.head = head self.anchor = anchor + def __repr__(self): + return "({}~{})".format(str(self.head), str(self.anchor)) + @property def genus(self): return self.anchor.genus diff --git a/lib/Bastion/Movers/BFD.py b/lib/Bastion/Movers/BFD.py index f946841..02874d7 100644 --- a/lib/Bastion/Movers/BFD.py +++ b/lib/Bastion/Movers/BFD.py @@ -11,8 +11,7 @@ class Mover(isMover): def __init__(self, vault, **kwargs): - isMover.__init__(self) - self.vault = vault + isMover.__init__(self, vault) self.bank = kwargs.get('bank', vault.bank) assert isinstance(self.vault, isVault), "vault must be an instance of Bastion.Model.isVault" diff --git a/lib/Bastion/Packers/TARs.py b/lib/Bastion/Packers/TARs.py index d30e18c..e9782cb 100644 --- a/lib/Bastion/Packers/TARs.py +++ b/lib/Bastion/Packers/TARs.py @@ -126,11 +126,11 @@ def pack(self, subject, *args, **kwargs): #-- use the built-in python tar archiver, using "pax" (POSIX.1-2001) extensions. pax(tarp, request.asset.halo, **kwargs) except Exception as err: - report = Report.Failed(request, err) + report = request.failed(err) else: #-- create an answer frame that will be part of the report. packed = PackingReceipt(request.asset, tag, catalog(tarp), tarp) - report = Report.Success(request, packed) + report = request.succeeded(packed) #-- answer the BLONDE of the newly created package. return report @@ -142,7 +142,7 @@ def unpack(self, subject, *args, **kwargs): .unpack(request) .unpack(halo, root) """ - raise NotImplementedError + raise NotImplementedError(".unpack is subclass responsibility") def catalog(tarp): diff --git a/lib/Bastion/Vaults/CARP.py b/lib/Bastion/Vaults/CARP.py index 44c7f98..dd4ab35 100644 --- a/lib/Bastion/Vaults/CARP.py +++ b/lib/Bastion/Vaults/CARP.py @@ -1,16 +1,17 @@ import logging import datetime -from Bastion.CARP import isRequest, isReceiptOfResults, isReport +from Bastion.CARP import isRequest, isWorkflowReceipt, Report from Bastion.Curator import BLONDE logger = logging.getLogger(__name__) -class PushReceipt(isReceiptOfResults): +class PushReceipt(isWorkflowReceipt): pass -class PushReport(isReport): + +class PushReport(Report): #---------------------------------------------------------------------- #-- BEGIN ACCESSORS | #-- These properties are pass throughs to the initial request object. | diff --git a/lib/Bastion/Vaults/Common.py b/lib/Bastion/Vaults/Common.py index 48162b2..be1a538 100644 --- a/lib/Bastion/Vaults/Common.py +++ b/lib/Bastion/Vaults/Common.py @@ -33,23 +33,29 @@ def push(self, asset, basis = None, **kwargs): {asset} - an instance of Bastion.Model.isAsset {basis} - can be a datetime or a BLONDE. """ - #-- Build the request object. + #-- Build the request object and an empty (anticipating!) receipt. request = PushRequest(asset, basis, **kwargs) + receipt = PushReceipt() + + #-- Do the pack activity. packing = self.pack(asset, basis, **kwargs) + receipt.append(packing, 'packed') + if packing.failed: - return request.failed( PushReceipt([('packed',packing)]) ) + return request.failed(receipt) - movement = self.put(packing.record.tarp, packing.record.tag) + movement = self.put(packing.record.tarp, packing.record.tag) + receipt.append(movement, 'moved') #-- clean up! if packing.record.tarp.exists(): packing.record.tarp.unlink() if movement.failed: - return request.failed( PushReceipt([('packed', packing), ('moved', movement)]) ) + return request.failed( PushReceipt(tag(packing, 'packed'), tag(movement, 'moved')) ) else: - return request.succeeded( PushReceipt([('packed', packing), ('moved', movement)]) ) + return request.succeeded( PushReceipt(tag(packing, 'packed'), tag(movement, 'moved')) ) def pull(self, ark, **kwargs): raise NotImplementedError