Python API#
This page describes some of the Python utility modules for writing your own custom autopilot app or a Custom Python Scripts with PAPSR.
yaku.autopilot_utils.cli_base#
A utility module for writing Python apps.
Python App Template#
This module provides some utility functions for easily creating Python apps without having to write a lot of boilerplate code for command line argument parsing, logging setup, result handling, etc.
There are example applications available in the tests/app_* folders.
Simple App#
A very simple (fetcher) app looks like this:
# Module yaku.app_single_command.cli
import click
from yaku.autopilot_utils.cli_base import make_autopilot_app, read_version_from_package
from loguru import logger
class CLI:
click_name = "app_single_command"
click_help_text = "Simple demo program for test purposes with just a simple command."
click_setup = [
click.option("--fail", is_flag=True),
]
@staticmethod
def click_command(fail: bool):
logger.info("Inside click_command")
if fail:
raise Exception("Failing as requested...")
logger.info("Should be doing something useful here!")
cli = make_autopilot_app(
provider=CLI,
version_callback=read_version_from_package(__package__)
)
if __name__ == "__main__":
cli()
The BUILD file looks like this:
pex_binary(
name="app_single_command",
entry_point="yaku.app_single_command.cli", # the name of the module above
)
All the setup of the app happens in the make_autopilot_app() function.
It requires two arguments:
a
providerwhich can be a class or a module.a
package_with_version_file, which must be a package name inside which a_version.txtfile is located which contains the app version number.
The provider class (could also be a module) usually has three attributes:
A
click_namestring which contains the app name (should be the same as the name of the pex binary in theBUILDfile).A
click_setup(which can be omitted or an empty list) which contains a list ofclick.optionorclick.argumentdecorators (but omit the@). Use those decorators to add arguments or options to your app.A
click_commandfunction (which can be missing if there are subcommands) which must accept the arguments according to theclick_setuplist and which contains the app logic.
For more complex use-cases, other attributes are also possible:
click_evaluator_callbackneeds to be defined if results are collected during the execution of theclick_commandfunction.click_subcommandscan be used to define subcommands for a main command, e.g. when having an Excel evaluator which can be called like excel-evaluate cell --location=A1 --equals="yes" or like excel-evaluate column --column-index=F --values="yes|no".
Click argument validation#
When using the validation callback=... argument for click.option, you need
to adhere to the way how click is handling exceptions.
Instead of raising
AutopilotConfigurationError or similar
exceptions from yaku.autopilot_utils.errors, use click.BadParameter
or click.UsageError instead.
For details, see ClickUsageErrorHandlerDecorator below.
Complex apps/evaluators#
There are multiple app configurations possible:
Simple app with a single command, not evaluation (see app_single_command/).
Simple evaluator, called from a single command (see app_single_evaluator/).
Complex app with multiple subcommands, but no evaluation (see app_multi_command/).
Complex evaluator with multiple independent evaluators which can not be chained (see app_multi_evaluator/). This means that you can only call one of the subcommands at a time. This also means that each evaluator needs to compute its own evaluation result (that’s why the subcommand providers in this example have all a custom
click_evaluator_callbackfunction)Complex evaluator with chainable subcommand evaluators. (see app_chained_multi_evaluator/). This means that you can call all the subcommands on the command line in a row. But this also means that the results generated by these subcommands need to be evaluated by the main command provider, and not by each of the subcommand providers. So only the main provider needs a
click_evaluator_callbackfunction.
Module contents#
- yaku.autopilot_utils.cli_base.make_autopilot_app(provider, *, version_callback, allow_chaining=True)#
Create a click application from a special type of class/module.
- Parameters:
provider (a class or module which provides necessary attributes, e.g.,) –
click_setup,click_command,click_help_text, …version_callback (a function which returns the version number of) – the app. See also:
read_version_from_package().allow_chaining (Flag to enable or disable the possibility to run) – multiple evaluators as chained subcommands, so that the main CLI provider does an overall evaluation of the aggregated results of all subcommands. If this is disabled, only one subcommand at a time can be used.
- yaku.autopilot_utils.cli_base.read_version_from_package(package_with_version_file, version_file='_version.txt')#
Return a function which reads a version number from a file in a Python package.
To be used as
version_callbackin themake_autopilot_app()function.Example:
cli = make_autopilot_app( provider=MyCliClass, version_callback=read_version_from_package("mycompany.mypackage", "version.txt"), )
- yaku.autopilot_utils.cli_base.ClickUsageErrorHandlerDecorator(f)#
Special decorator which wraps around the outermost
click.commanddecorator.This wrapping is done automatically by the
make_autopilot_app()function.This is a necessary workaround, because input parameter validation happens inside
click.command. Usually,clickhandles parameter validation errors on its own, e.g. when aclick.UsageErrororclick.BadParameteris raised inside a validationcallbackfunction, it automatically prints out the command usage instructions, together with the exception’s message and exits with an exit code of 2.However we want to deal with usage errors differently in our autopilot interface:
we want to exit with code 0 in case of expected errors
we want to print out a JSON line with
status=FAILEDand a proper reason.
This is why this decorator exists: it wraps the outermost
click.command(orclick.group) decorator and simply forwards all unknown accesses to our object to the underlyingfobject (inside the__getattr__function).There are two possible invocation paths:
In case of testing, the
mainattribute is called.In case of normal CLI invocation, the object/function is simply called.
That’s why accesses to
mainare rerouted as well as__call__(in case the class is called).Both accesses call the
_wrapper_error_handlermethod, which has the correct logic for filtering outSystemExitexceptions which have a__context__coming from aclick.exceptions.UsageError.Only in this case, the exception is modified to become a
SystemExit(0)and a proper JSON line is printed.All other cases are simply re-raised at the end of the
_wrapper_error_handlermethod.
yaku.autopilot_utils.subprocess#
Support for running other autopilot apps as subprocesses.
When developing Custom Python Scripts, it might be necessary to call other autopilot apps. As they are standalone applications, they can only be called through normal system subprocesses.
To support this use-case better, this module provides a special run() function
which mimics the builtin subprocess.run function, but extends the result
of this function by some extra fields, e.g. for getting the list of outputs
from an autopilot call. See run() for details.
As an example, we want to run two autopilot apps in a row and provide a special result message if both succeed:
# first we call the other app
step1 = run(["sharepoint-fetcher"])
# if the other app returned a non-zero exit code, the next line
# would immediately exit the program and print the other app's
# stdout and stderr log
step1.exit_for_returncode()
# if the other app returned a non-GREEN, non-RED, or non-YELLOW status,
# we can use this line to raise an `AutopilotSubprocessFailure` and stop
# any further processing in our current function.
# This would still print the other app's stdout and stderr log as well
# as the outputs and results of the other app.
step1.raise_for_status()
# now, we can (but don't have to) add the results from step 1
# to the main log so that they appear later in the HTML result
for r in step1.results:
RESULTS.append(r)
# then we do the same for the second app
step2 = run(["artifactory-fetcher"])
step2.exit_for_returncode()
step2.raise_for_status()
for r in step2.results:
RESULTS.append(r)
# and finally, when both previous steps have succeeded (GREEN or YELLOW)
# we can prepare a final message:
RESULTS.append(
Result(
criterion="Both files are fetched correctly.",
fulfilled=True,
justification="Both fetchers finished successfully.",
)
)
Of course, this final step could also be done in the click_evaluator_callback,
but the logic above allows full flexibility on the flow control between the
different subprocesses. Also, _outputs_ could be retrieved from step1 and
used for step2.
- yaku.autopilot_utils.subprocess.run(command, /, shell=False, extra_env=None, **kwargs)#
Run another autopilot app in a subprocess.
This function is a wrapper around
subprocess.run, so it executes the givencommandand passes anykwargsto thesubprocess.runfunction.Motivation for this wrapper was that there should be an easy way to call an autopilot app as a subfunction and access its status and results:
result = run(["sharepoint-fetcher"]) if result.status == 'GREEN': ... if result.results[0].fulfilled: ... # and so on
After the command has finished, it parses the resulting stdout and extracts
status,reason,clean_stdout,resultsdata and attaches it to the returned object.Additionally, it provides two functions in the returned object: :rtype:
ProcessResultProcessResult.exit_for_returncode()can be called if you want to do a system exit in case of a non-zero return code.ProcessResult.raise_for_status()which raises aAutopilotSubprocessFailureif the subprocess app status is notGREEN,YELLOW, orRED(Noneis ignored as well).
- class yaku.autopilot_utils.subprocess.ProcessResult(*args, **kwargs)#
Result of a subprocess run command.
Contains all attributes and methods from the built-in
subprocess.runfunction.Has extra attributes:
status: The result status of the autopilot app, e.g.GREENorRED.reason: The reason text for the status.clean_stdout: A cleaned version of thestdoutoutput, where all JSON lines have been removed.results: A list ofResultobjects with the collected results of the autopilot app.outputs: AnOutputMapcontaining all autopilot outputs.
There are also two useful extra methods:
exit_for_returncode(): for returncodes unequal to zero, causes a program termination when called.raise_for_status(): will raise an exception if there is no valid status.
Utilities#
- exception yaku.autopilot_utils.subprocess.AutopilotSubprocessFailure(process_result)#
Indicate a failing subprocess.
If a subprocess fails, it is often required to skip any following checks. This exception can be used to indicate that some steps have failed and that evaluation should be done immediately.
yaku.autopilot_utils.results#
Handle evaluator results.
Usage#
Just import the RESULTS singleton and append results to it:
from yaku.autopilot_utils.results import RESULTS, Result
RESULTS.append(Result(criterion="...", fulfilled=False, justification="..."))
When using the click app template in [cli_base.py](./cli_base.py), make
sure to define a click_evaluator_callback which receives the collected
results and must return a status and a reason:
def click_evaluator_callback(results: ResultsCollector) -> Tuple[str, str]:
for result in results:
# ... examine results
# ... and define reason, status
return status, reason
Testing#
During tests, when modifying the RESULTS singleton, it must be
reset after the test. This can be done with the protect_results
decorator which you simply put around your test function:
from yaku.autopilot_utils.results import RESULTS, protect_results
@protect_results
def test_result_handling():
# do something with RESULTS
RESULTS.append(...)
# after the test has finished, RESULTS will be reset to previous state
Module contents#
- yaku.autopilot_utils.results.RESULTS = []#
Singleton for storing and accessing results.
You can use it in your autopilot to store results for later evaluation:
RESULTS.append(Result(criterion="...", fulfilled=False, justification="..."))
- yaku.autopilot_utils.results.DEFAULT_EVALUATOR(results)#
Evaluate results and return RED status if any criterion is not fulfilled.
This is the default implementation of the evaluator and can be used in papsr or autopilot apps. Simply use it as:
class CLI: click_evaluator_callback = DEFAULT_EVALUATOR
- class yaku.autopilot_utils.results.Output(key, value)#
Simple data class to store
keyandvalueof an output.Has an extra method
to_jsonto convert the output data back to a JSON line.
- class yaku.autopilot_utils.results.Result(criterion, fulfilled, justification, metadata=<factory>)#
Simple data class to store a
resultof an autopilot app.Has fields
criterion,fulfilled, andjustification.
Test helpers#
- yaku.autopilot_utils.results.assert_no_result_status(output)#
Assert that there is no JSON line with a status in the output.
- yaku.autopilot_utils.results.assert_result_status(output, expected_status, reason=None)#
Parse JSON lines in output and look for status and reason properties.
This function is a testing utility when you want to verify autopilot output. It parses the output and looks for JSON lines and then verifies that there is a
statusproperty with theexpected_statusand optionally it matches thereasonproperty against a regular expression given inreason.
yaku.autopilot_utils.errors#
The following classes are used to differentiate between different error cases.
Collection of various exception classes for reporting different types of autopilot errors.
There are two main types of exception classes:
AutopilotException(used for wrapping internal failures into more helpful error messages)AutopilotFailure(for all errors which the user might be able to resolve, e.g. configuration mistakes)
- exception yaku.autopilot_utils.errors.AutopilotException#
Base class for all autopilot errors.
Those errors are not treated differently than normal Python exceptions. This base class is just there so that developers can use it to wrap other (built-in) exceptions into a custom exception with a custom error message.
- exception yaku.autopilot_utils.errors.AutopilotError#
Exception for (more or less) unexpected autopilot errors.
For example can be used to annotate builtin exceptions with extra message, e.g.:
try: ... except Exception as e: raise AutopilotError("During ..., an error occurred!") from e
- exception yaku.autopilot_utils.errors.AutopilotFailure(reason)#
Base class for all autopilot failures.
Will immediately exit the command line invocation and print out a ‘FAILED’ status with the given reason. (this is implemented in yaku.autopilot_utils.cli_base)
Will not print out a stack trace as normal exceptions. (this is implemented in yaku.autopilot_utils.cli_base)
- exception yaku.autopilot_utils.errors.AutopilotConfigurationError(reason)#
Errors related to autopilot configuration.
- exception yaku.autopilot_utils.errors.EnvironmentVariableError(reason)#
Errors related to environment variables.
- exception yaku.autopilot_utils.errors.AutopilotFileNotFoundError(reason)#
A required file is not found.
- exception yaku.autopilot_utils.errors.FileNotFoundError(reason)#
A required file is not found.
Deprecated: use
AutopilotFileNotFoundErrorinstead.
yaku.autopilot_utils.environment#
- yaku.autopilot_utils.environment.require_environment_variable(variable_name)#
Ensure that an environment variable is there and return its value.
- Return type:
str