a lib for describing Actions and how they should be performed
Side effects are annoying. Verification of intended outcome is often difficult and can depend on the system's state at runtime. Questions like "Is the file going to be present when data is written?" or "Will that service be available?" come to mind. Keeping track of external system state is just impractical, but declaring intent and encapsulating its disposition is doable.
Action
objects are used to declare intent:
>>> action = Read('path/to/some/file')
The action
, above, represents the intent to Read
the contents from the file at some path.
An Action
can be "performed" and the result is captured by a Result
object:
>>> result = action.perform() # produces a Result object
The result
holds disposition information about the outcome of the action
.
That includes information like whether or not it was .successful
or that it was .produced_at
some unix timestamp (microseconds by default).
To gain access to the value of the result
, check the .value
attribute.
If unsuccessful, there will be an Exception
, otherwise there will be an instance of some non-Exception
type.
A Result
can be produced by performing an Action
and that value can be percolated through a collection of ActionTypes
using the Pipeline
abstraction:
>>> pipeline = Pipeline(ReadInput('Which file? '), Read)
The above, is not the most helpful incantation, but toss the following in a while
loop and witness some REPL-like behavior (bonus points for feeding it actual filenames/filepaths).
result = Pipeline(ReadInput('Which file? '), Read).perform()
print(result.value)
Sometimes ActionType
s in a Pipeline
don't "fit" together.
That's where the Pipeline.Fitting
comes in:
listen = ReadInput('What should I record? ')
record = Pipeline.Fitting(
action=Write,
**{
'prefix': f'[{datetime.now()}] ',
'append': True,
'filename': filename,
'to_write': Pipeline.Receiver
},
)
Pipeline(listen, record).perform()
⚠️ NOTE: Writing to stdout is also possible using theWrite.STDOUT
object as a filename. How that works is an exercise left for the user.
An Action
collection can be used to describe a procedure:
actions = [action,
Read('path/to/some/other/file'),
ReadInput('>>> how goes? <<<\n > '),
MakeRequest('GET', 'http://google.com'),
RetryPolicy(MakeRequest('GET', 'http://bad-connectivity.com'),
max_retries=2,
delay_between_attempts=2)
Write('path/to/yet/another/file', 'sup')]
procedure = Procedure(actions)
And a Procedure
can be executed synchronously or otherwise:
results = procedure.execute() # synchronously by default
_results = procedure.execute(synchronously=False) # async; not thread safe
result = next(results)
print(result.value)
A KeyedProcedure
is just a Procedure
comprised of named Action
s.
The Action
names are used as keys for convenient result lookup.
prompt = '>>> sure, I'll save it for ya.. <<<\n > '
saveme = ReadInput(prompt).set(name='saveme')
writeme = Write('path/to/yet/another/file', 'sup').set(name='writeme')
actions = [saveme, writeme]
keyed_procedure = KeyedProcedure(actions)
results = keyed_procedure.execute()
keyed_results = dict(results)
first, second = keyed_results.get('saveme'), keyed_results.get('writeme')
⚠️ NOTE:Procedure
elements are evaluated independently unlike with aPipeline
in which the result of performing anAction
is passed to the nextActionType
.
One can also create an Action
from some arbitrary function
>>> Call(closure=Closure(some_function, arg, kwarg=kwarg))
Build scripting is managed via noxfile
.
Execute nox -l
to see the available commands (set the USEVENV
environment variable to view virtualenv-oriented commands).
To get started, simply run nox
.
Doing so will install actionpack
on your PYTHONPATH.
Using the USEVENV
environment variable, a virtualenv can be created in the local ".nox/" directory with something like:
USEVENV=virtualenv nox -s actionpack-venv-install-3.10
All tests can be run with nox -s test
and a single test can be run with something like the following:
TESTNAME=<tests-subdir>.<test-module>.<class-name>.<method-name> nox -s test
Coverage reports are optional and can be disabled using the COVERAGE
environment variable set to a falsy value like "no".
Making new actionpack.actions
is straightforward.
After defining a class that inherits Action
, ensure it has an .instruction
method.
If any attribute validation is desired, a .validate
method can be added.
There is no need to add Action
dependencies to setup.py
.
Dependencies required for developing an Action
go in :::drum roll::: requirements.txt
.
When declaring your Action
class, a requires
parameter can be passed a tuple.
class MakeRequest(Action, requires=('requests',)):
...
This will check if the dependencies are installed and, if so, will register each of them as class attributes.
mr = MakeRequest('GET', 'http://localhost')
mr.requests #=> <module 'requests' from '~/actionpack/actionpack-venv/lib/python3/site-packages/requests/__init__.py'>