yaml_requests

A simple python app for sending a set of consecutive HTTP requests defined in YAML requests plan.

Installing

Ensure that you are using Python >= 3.9 with python --version. This app/package is available in PyPI. To install, run:

pip install yaml_requests

Usage

The app is used to execute HTTP requests defined in YAML files. The YAML file must contain main-level key requests, that contains an array of requests, where each item of the list is a request object. The request object contains at least a method key (get, post, options, ...) which value is passed to requests.request function, or to requests.Session.request if plan level option session is truthy.

Minimal YAML request plan should thus include requests array, with single item in it:

requests:
- get:
    url: https://google.com

In addition to this basic behavior, more advanced features are provided as well:

  • All value fields in requests array items support jinja2 templating.
  • Values can be read from environment variables with lookup function. For example, {{ lookup("env", "API_TOKEN") }}.
  • Files can be read as text with lookup function (e.g., {{ lookup("file", "headers.yaml")}}) or opened with open function (e.g. {{ open("photos/eiffer-tower.jpg") }}) to pass in as file objects to files parameter of request functions.
  • Variables can be defined in YAML request plan and overridden from commandline arguments.
  • Response of the most recent request is stored in response variable as requests.Response object.
  • Responses can be stored as variables with register keyword.
  • Response can be verified with assertions.
  • Plan execution can be repeated by setting repeat_while option.
  • Request can be looped by defining loop option for a request. The current item is available in item variable.

API Reference

 1"""
 2.. include:: ../README.md
 3    :start-after: <!-- Start docs include -->
 4    :end-before: <!-- End docs include -->
 5
 6## API Reference
 7"""
 8
 9from importlib.metadata import version
10__version__ = version('yaml_requests')
11
12from ._main import execute, main, run
13from ._plan import Plan, PlanOptions
14from ._request import Assertion, Request
15
16
17# Hide dataclass constructors from documentation.
18for i in (Plan, PlanOptions, Assertion, Request):
19    i.__init__.__doc__ = '@private'
20
21
22__all__ = [
23    'error',
24    'Plan',
25    'PlanOptions',
26    'Request',
27    'Assertion',
28]
@dataclass
class Plan:
 88@dataclass
 89class Plan:
 90    '''A plan that contains requests to be executed consecutively.'''
 91
 92    name: str
 93    '''Human readable description for the plan.'''
 94    path: str
 95    '''@private Path to the plan file.'''
 96    options: PlanOptions
 97    '''Options for controlling the execution of the plan. See `PlanOptions`
 98    for details.'''
 99    variable_files: list[str]
100    '''List of paths to variable files. The variable file can be relative to
101    either the current working directory or the plan file. The variable file
102    must be a JSON or YAML file.
103
104    See `variables` for variable precedence.
105    '''
106    variables: dict
107    '''Variables to be used in the plan.
108
109    Variable precedence from least to greatest:
110
111    1. Variables defined in the `variables` field of the plan.
112    2. Variables defined in the variable files.
113    3. Variables defined in the shell command with `-v`/`--variable` option.
114    '''
115    requests: list[Request]
116    '''List of requests to be executed.'''
117
118    @classmethod
119    def _from_dict(
120            cls,
121            input_dict,
122            options_override=None,
123            variables_override=None):
124        if variables_override is None:
125            variables_override = {}
126
127        plan_dict = deepcopy(input_dict)
128
129        path = plan_dict.pop('path', None)
130        variable_files = _resolve_variable_files(
131            plan_dict.get('variable_files'), path)
132        variables = {
133            **plan_dict.get('variables', {}),
134            **_load_variable_files(variable_files),
135            **variables_override
136        }
137
138        requests = plan_dict.get('requests')
139        if not requests or not isinstance(requests, list):
140            raise AssertionError('Plan must contain requests array.')
141
142        return cls(
143            name=plan_dict.pop('name', None),
144            path=path,
145            options=PlanOptions._from_dict(
146                plan_dict.get('options'), options_override),
147            variable_files=variable_files,
148            variables=variables,
149            requests=requests
150        )
151
152    def _title(self, display_filename=False):
153        if not display_filename:
154            return self.name
155
156        if self.name and self.path:
157            return f'{self.name} ({self.path})'
158
159        return self.path or self.name

A plan that contains requests to be executed consecutively.

name: str

Human readable description for the plan.

options: PlanOptions

Options for controlling the execution of the plan. See PlanOptions for details.

variable_files: list[str]

List of paths to variable files. The variable file can be relative to either the current working directory or the plan file. The variable file must be a JSON or YAML file.

See variables for variable precedence.

variables: dict

Variables to be used in the plan.

Variable precedence from least to greatest:

  1. Variables defined in the variables field of the plan.
  2. Variables defined in the variable files.
  3. Variables defined in the shell command with -v/--variable option.
requests: list[Request]

List of requests to be executed.

@dataclass
class PlanOptions:
17@dataclass
18class PlanOptions:
19    '''Options for controlling the execution of the plan.'''
20
21    session: bool = False
22    '''Use session to keep cookies between requests.'''
23    ignore_errors: bool = None
24    '''Continue executing requests even if one of them fails.'''
25    repeat_while: str = None
26    '''Expression that determines if the plan should be repeated.'''
27    repeat_delay: int = None
28    '''Time to sleep in seconds before repeating the plan.'''
29
30    @classmethod
31    def _from_dict(cls, options_dict=None, options_override=None):
32        if options_dict is None:
33            options_dict = {}
34
35        if options_override is None:
36            options_override = {}
37
38        return cls(**{**options_dict, **options_override})

Options for controlling the execution of the plan.

session: bool = False

Use session to keep cookies between requests.

ignore_errors: bool = None

Continue executing requests even if one of them fails.

repeat_while: str = None

Expression that determines if the plan should be repeated.

repeat_delay: int = None

Time to sleep in seconds before repeating the plan.

@dataclass
class Request:
116@dataclass
117class Request:
118    '''A Request to send.'''
119
120    name: str
121    '''Human readable description for the request.'''
122    method: str
123    '''HTTP method to use.
124
125    Can also be defined as a main level dict key with `params` as the value.
126    For example:
127
128    ```yaml
129    - name: Get index page
130      get:
131        url: http://localhost:8080
132    ```'''
133    params: dict
134    '''Parameters to pass to the `requests.request` function.
135
136    Can also be defined as a main level dict value with the HTTP `method` as
137    the key. See `method` for details.'''
138    loop: str
139    '''Loop over the given list of items.'''
140    assertions: list[Union[Assertion, str]]
141    '''List of assertions to execute after the request is sent.
142
143    Can also be defined with `assert` key.'''
144    register: str = None
145    '''Register the response object as a variable with the given name.'''
146    raise_for_status: bool = True
147    '''Raise an exception if the response status code is not ok.'''
148    output: str = None
149    '''Output the given properties of the response, e.g. `response_json`.'''
150
151    def _parse_options(self, request_dict):
152        self.register = request_dict.get('register')
153        self.raise_for_status = request_dict.get('raise_for_status', True)
154        self.output = request_dict.get('output')

A Request to send.

name: str

Human readable description for the request.

method: str

HTTP method to use.

Can also be defined as a main level dict key with params as the value. For example:

- name: Get index page
  get:
    url: http://localhost:8080
params: dict

Parameters to pass to the requests.request function.

Can also be defined as a main level dict value with the HTTP method as the key. See method for details.

loop: str

Loop over the given list of items.

assertions: list[typing.Union[Assertion, str]]

List of assertions to execute after the request is sent.

Can also be defined with assert key.

register: str = None

Register the response object as a variable with the given name.

raise_for_status: bool = True

Raise an exception if the response status code is not ok.

output: str = None

Output the given properties of the response, e.g. response_json.

@dataclass
class Assertion:
61@dataclass
62class Assertion:
63    '''An assertion to execute after the request is sent. The assertion is
64    considered successful if the expression evaluates to `True`.
65
66    For example:
67
68    ```yaml
69    - name: Response is not empty
70      expression: response.json() | length
71    ```
72    '''
73
74    name: str
75    '''Human readable description for the assertion.'''
76    expression: str
77    '''Expression to evaluate.'''

An assertion to execute after the request is sent. The assertion is considered successful if the expression evaluates to True.

For example:

- name: Response is not empty
  expression: response.json() | length
name: str

Human readable description for the assertion.

expression: str

Expression to evaluate.