Skip to content

Commit

Permalink
Added an idiom class for pattern matching vs. command line;
Browse files Browse the repository at this point in the history
extending isRequest to read command line;
re-established hasCanonicalForm and fromCanonicalForm as trait classes to signal toJDN capabilities;
tightened up requirements for records (in results) to have hasCanonicalForm traits;
tar catalog is now an object (not a list).
  • Loading branch information
ndenny committed Jun 5, 2025
1 parent 51fa2dd commit 694e0a7
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 37 deletions.
44 changes: 27 additions & 17 deletions bin/bastion.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@
a. differential if drift < policy
b. anchor (full) if drift >= policy
"""
class Commander:
def __init__(self, **kwargs):
self.simulate = kwargs.get('simulate', False)

def do(self, cmd):
if self.simulate:
logger.debug("simulating... {}".format(cmd))
xcode = 0
else:
logger.debug("executing... {}".format(cmd))
xcode = os.system(cmd)
return xcode


class ProtectionRequest(Request):
"""
Expand Down Expand Up @@ -981,33 +994,33 @@ def do_become_conductor(self, request):
I enter into a forever loop, napping for 15 second intervals, then waking to check the time and do any necessary housekeeping and backup operations.
"""
simulated = ('simulate!' in request.args.values()) or ('simulated!' in request.args.values())
commander = Commander(simulate = simulated)
lastCheckIn = datetime.datetime(1, 1, 1, 0, 0, 0) #-- 1 JAN 1 CE

while ALWAYS:
now = datetime.datetime.now()
logger.debug("tick-tock {}".format(now.isoformat()))

if (now - lastCheckIn) < (24*HOURS):
time.sleep(15.0) #-- Sleep 15 seconds.

else:
#-- A brand new day!

for site in self.sites:
#-- Update the asset list for all of the sites that I'm managing...
for zone in site.zones:
if zone.dynamic:
if zone.isDynamic:
ecommand = '{}/bastion.py enroll assets "{}"'.format(str(BIN_PATH), str(zone.ARK))
if not simulated:
logger.debug("executing... {}".format(ecommand))
os.system(ecommand)
else:
logger.debug("simulating... {}".format(ecommand))
commander.do( ecommand )

for site in self.sites:
#-- Queue assets for updates for all of the sites that I'm managing...
for zone in site.zones:
#-- Assets are queued by zone...
qcommand = '{}/bastion.py queue assets "{}"'.format(str(BIN_PATH), str(zone.ARK))
if not simulated:
logger.debug("executing... {}".format(qcommand))
os.system(qcommand)
else:
logger.debug("simulating... {}".format(qcommand))
commander.do( qcommand )

for site in self.sites:
#-- Spawn a subprocess to pop all of the objects in the queue for each siteXzone combo....
for zone in site.zones:
Expand All @@ -1016,11 +1029,8 @@ def do_become_conductor(self, request):
#-- if IONICE is defined, then run the bank operations with IO niced to "Idle",
#-- i.e. disk access for banking is only done when no other processes need the disk.
bcommand = "{} -c 3 -t {}".format(str(IONICE), bcommand)
if not simulated:
logger.debug("executing... {}".format(bcommand))
os.system(bcommand)
else:
logger.debug("simulating... {}".format(bcommand))
commander.do( bcommand )

lastCheckIn = datetime.datetime.now()


Expand Down Expand Up @@ -1091,4 +1101,4 @@ def do_become_conductor(self, request):

#bastion perform request filed
#bastion perform request queued
#bastion become conductor
#bastion become conductor
128 changes: 122 additions & 6 deletions lib/Bastion/CARP.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

from collections.abc import Sequence

from Bastion.Common import toJDN, isBoxedObject, isTagged
from Bastion.Common import toJDN, hasCanonicalForm, isBoxedObject, isTagged


class ReplyStatus(tuple):
Expand Down Expand Up @@ -134,22 +134,98 @@ def is_inconclusive(self):
ResultStatus = ReplyStatus #-- Alias for ReplyStatus


class isRequest:
class Idiom(tuple):
def __new__(cls, idiom):
if isinstance(idiom, Idiom):
return idiom
elif isinstance(idiom, str):
tokens = [token.strip() for token in idiom.split()]
return tuple.__new__(cls, tokens)
else:
raise ValueError

def slots(self):
if not hasattr(self, "_slots"):
slots = [ ]
for token in self:
if (token[0] == '_') and (token[-1] == '_'):
slots.append(token[1:-1])
self._slots = tuple(slots)
return self._slots

@property
def priority(self):
weight = len(self)
density = len([token for token in self if ((token[0] != '_') and (token[0] != '...'))])
return (weight, density)

def __str__(self):
return " ".join(self)

def __repr__(self):
return "🧠:{}".format(str(self))

def __mod__(self, other):
"""
congruence operator.
i.e. Idiom("_ help") % "get help" -> (Idiom("_help"), ["get"])
"""
words = [word.strip() for word in other.split()]

if len(self) > len(words):
#-- I have more tokens than other, I can't possibly be a match for other.
return None

planks = [ ]
args = { }

for i, token in enumerate(self):
word = words.pop(0)
if token == '...':
remainder = [word] + words
planks.extend(remainder)
args['...'] = " ".join(remainder)
return (self, args, tuple(fillers))
elif token == '_':
planks.append(word)
elif (len(token) > 1) and (token[0] == '_') and (token[-1] == '_'):
slot = token[1:-1]
args[slot] = word
planks.append(word)
elif token != word:
return None

remainder = words

return (self, args, tuple(planks), remainder)

#-- Some ideas for testing Idiom...
#menu = [Idiom(m) for m in ["help", "help ...", "bank asset _ark_", "update asset _ark_"]]
#menu = sorted(menu, key = lambda i: i.priority, reverse = True)

class isRequest(hasCanonicalForm):
def __init__(self, action, *args, **kwargs):
self.ID = kwargs.get('ID', str(uuid.uuid4()))
self.action = action
self.when = kwargs.get('opened', datetime.datetime.now())
self.args = dict(enumerate(args))
self.context = { }
self.flags = set()

excluded = {'ID', 'opened', 'context'}
excluded = {'ID', 'opened', 'context', 'flags'}
for k, v in kwargs.items():
if k not in excluded:
self.context[k] = v

if 'context' in kwargs:
for k, v in kwargs['context'].items():
self.context[k] = v

if 'flags' in kwargs:
self.flags.update(kwargs['flags'])
for flag in self.flags:
self.context[flag] = True

@property
def lede(self):
return ' '.join( [self.action] + [self.args[i] for i in sorted(self.args.keys())] )
Expand All @@ -166,7 +242,8 @@ def toJDN(self, **kwargs):
'action': self.action,
'args': { },
'opened': self.when.isoformat(),
'context': { }
'context': { },
'flags': list(self.flags)
}
if self.args:
for k, v in self.args.items():
Expand All @@ -189,10 +266,49 @@ def crashed(self, record = None, *args, **kwargs):
def inconclusive(self, record = None, *args, **kwargs):
return Report.Inconclusive(self, record, *args, **kwargs)

@classmethod
def fromCLI(cls, argls = None, **kwargs)
"""
Convenience constructor for creating a request directly from the command line (sys.argv)
e.g.
request = Request.fromCLI()
"""
flags = []
opts = { }
idioms = kwargs.get('idioms', [])

if argls is None:
argls = sys.argv[1:]

if '--' in argls:
i = argls.index('--')
comargls = argls[:i]
optl = argls[i+1:]
for word in optls:
if word[-1] == '!':
flag = word[:-1]
flags.append(flag)
if ':' in word:
j = word.index(':')
var = word[:j]
val = word[j+1:]
opts[var] = val

#-- by default, action is the concatenation of the command arg list.
action = " ".join(comargls)
#-- However, if we have a list of idioms to match...
if idioms:
menu = [Idiom(m) for m in in idioms]
for idiom in menu:
if idiom % opts:

return cls(action, *comargls, context = opts, flags = flags)


#-- legacy alias
Request = isRequest

class isResult:
class isResult(hasCanonicalForm):
def __init__(self, request, status, record, **kwargs):
#-- POSITIONAL ARGS...
#-- request is a REQUIRED 2-tuple of (action, [arg1, ...])
Expand Down Expand Up @@ -345,7 +461,7 @@ def __init__(self, request, *args, **kwargs):



class isReceipt:
class isReceipt(hasCanonicalForm):
"""
I am an abstract base class for receipts.
Mostly, I'm used for run-time type checking.
Expand Down
4 changes: 2 additions & 2 deletions lib/Bastion/Common.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ def Slug40(text):
return base64.b32encode(bs).decode('utf-8')


class canStruct:
class hasCanonicalForm:
"""
I am a trait for serialization using JSON and YAML.
"""
Expand All @@ -365,7 +365,7 @@ def toYAML(self, **kwargs):
return yaml.dump(jdn, default_flow_style = False, indent = 3)


class canConStruct:
class fromCanonicalForm:
@classmethod
def fromJDN(cls, jdn, **kwargs):
raise NotImplementedError(".fromJDN is subclass responsibility")
Expand Down
4 changes: 3 additions & 1 deletion lib/Bastion/Packers/CARP.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import datetime
import logging

from Bastion.Common import toJDN, hasCanonicalForm
from Bastion.Model import isAsset
from Bastion.CARP import isRequest, isReceipt
from Bastion.Curator import BLONDE, Snap
Expand All @@ -19,6 +20,7 @@ def __init__(self, asset, tag, packaged, tarp):
assert isinstance(asset, isAsset), "asset must be an instance of Bastion.Model.isAsset"
assert isinstance(tag, pathlib.PurePosixPath), "tag must be pathlib.PurePosixPath"
assert isinstance(tarp, pathlib.PosixPath), "tarp must be pathlib.PosixPath"
assert isinstance(packaged, hasCanonicalForm), "package must have canonical form (e.g. toJDN)"

self.asset = asset #-- the asset that was packed
self.tag = tag #-- the full name of the packed object
Expand All @@ -30,7 +32,7 @@ def toJDN(self):
jdn = {
'asset': str(self.asset.CURIE),
'tag': str(self.tag),
'packaged': list(self.packaged),
'packaged': toJDN(self.packaged),
'blonde': str(self.blonde),
'tarp': str(self.tarp)
}
Expand Down
Loading

0 comments on commit 694e0a7

Please sign in to comment.