locust-grasshopper


Namelocust-grasshopper JSON
Version 1.3.27 PyPI version JSON
download
home_pageNone
Summarya load testing tool extended from locust
upload_time2024-07-31 07:22:16
maintainerNone
docs_urlNone
authorNone
requires_python<4,>=3.10
licenseApache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
keywords load testing performance locust grasshopper
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            <div id="top"></div>

# Grasshopper

A lightweight framework for performing load tests against an environment, primarily 
against an API. Grasshopper glues [Locust](https://locust.io/), [Pytest](https://docs.pytest.org/en/7.1.x/#), some plugins (namely [Locust InfluxDBListener](https://github.com/hoodoo-digital/locust-influxdb-listener) ) and some custom code to provide a
package that makes authoring load tests simple with very little boilerplate needed.

Here are some key functionalities that this project extends on Locust:
- [checks](#checks)
- [custom trends](#custom-trends)
- [timing thresholds](#thresholds)
- [streamlined metric reporting/tagging system](#db-reporting)
  (only influxDB is supported right now)

## Installation
This package can be installed via pip: `pip install locust-grasshopper`

## Example Load Test
- You can refer to the test `test_example.py` in the `example` directory for a basic 
  skeleton of how to get a load test running. In the same directory, there is also an 
  example `conftest.py` that will show you how to get basic parameterization working.
- This test can be invoked by running `pytest example/test_example.py` in the root of 
  this project.
- This test can also be invoked via a YAML scenario file:
```shell
cd example
pytest example_scenarios.YAML --tags=example1
```
 In this example scenario file, you can see how grasshopper_args, 
 grasshopper_scenario_args, and tags are being set.
<p align="right">(<a href="#top">back to top</a>)</p>

## Creating a load test
When creating a new load test, the primary grasshopper function you will be using 
is called `Grasshopper.launch_test`. This function can be imported like so: `from grasshopper.lib.grasshopper import Grasshopper`
`launch_test` takes in a wide variety of args:
- `user_classes`: User classes that the runner will run. These user classes must 
  extend BaseJourney, which is a grasshopper class 
  (`from grasshopper.lib.journeys.base_journey import BaseJourney`). This can be a 
  single class, a list of classes, or a dictionary where the key is the class and 
  the value is the locust weight to assign to that class.
- `**complete_configuration`: In order for the test to have the correct configuration, you 
  must pass in the kwargs provided by the `complete_configuration` fixture. See example 
  load test on how to do this properly.
<p align="right">(<a href="#top">back to top</a>)</p>

## Scenario Args   

- If you want to parameterize your journey class, you should use the `scenario_args` 
  dict. This is the proper way to pass in values from outside of 
  the journey for access by the journey code. Note that each journey gets a 
  ***copy*** on start, so the journey itself can safely modify its own dictionary 
  once the test is running.
  `scenario_args` exists for any journey that extends the grasshopper `base_journey` 
  class. 
  `scenario_args` also grabs from `self.defaults` on initialization. For example:
```python
from locust import between, task
from grasshopper.lib.journeys.base_journey import BaseJourney
from grasshopper.lib.grasshopper import Grasshopper

# a journey class with an example task
class ExampleJourney(BaseJourney):
    # number of seconds to wait between each task
    wait_time = between(min_wait=20, max_wait=30)
    
    # this defaults dictionary will be merged into scenario_args with lower precedence 
    # when the journey is initialized
    defaults = {
        "foo": "bar",
    }
    
    @task
    def example_task:
        logging.info(f'foo is `{self.scenario_args.get("foo")}`.')
        
        # aggregate all metrics for the below request under the name "get google"
        # if name is not specified, then the full url will be the name of the metric
        response = self.client.get('https://google.com', name='get google')

# the pytest test which launches the journey class
def test_run_example_journey(complete_configuration):
    # update scenario args before initialization
    ExampleJourney.update_incoming_scenario_args(complete_configuration)
    
    # launch the journey
    locust_env = Grasshopper.launch_test(ExampleJourney, **complete_configuration)
    return locust_env
```
<p align="right">(<a href="#top">back to top</a>)</p>

## Commonly used grasshopper pytest arguments
- `--runtime`: Number of seconds to run each test. Set to 120 by default.
- `--users`: Max number of users that are spawned. Set to 1 by default.
- `--spawn_rate` : Number of users to spawn per second. Set to 1 by default.
- `--shape`: The name of a shape to run for the test. 
If you don't specify a shape or shape instance, then the shape `Default` will be used, 
  which just runs with the users, runtime & spawn_rate specified on the command line (or picks up defaults 
of 1, 1, 120s). See `utils/shapes.py` for available shapes and information on how to add
your own shapes.
- `--scenario_file` If you want a yaml file where you pre-define some args, this is how 
you specify that file path. For example, 
  `scenario_file=example/scenario_example.YAML`. 
- `--scenario_name` If `--scenario_file` was specified, this is the scenario name that is 
within that YAML file that corresponds to the scenario you wish to run. Defaults to None.
- `--tags` See below example: `Loop through a collection of scenarios that match some 
  tag`.
- `--scenario_delay` Adds a delay in seconds between scenarios. Defaults to 0.
- `--influx_host` If you want to report your performance test metrics to some influxdb, 
you must specify a host.
    E.g. `1.1.1.1`. Defaults to None.
- `--influx_port`: Port for your `influx_host` in the case where it is non-default.
- `--influx_ssl`: If your influxdb is using SSL, set this to True. Defaults to False.
- `--influx_verify_ssl`: If your influxdb is using SSL, set this to True. Defaults to 
    False.
- `--influx_user`: Username for your `influx_host`, if you have one.
- `--influx_pwd`: Password for your `influx_host`, if you have one.
- `--grafana_host`: If your grafana is a separate URL from the influxdb, you can 
  specify it here. If you don't, then the grafana URL will be the same as the 
  influxdb URL when the grasshopper object generates grafana links. 

<p align="right">(<a href="#top">back to top</a>)</p>

## Launching tests with a configuration
All in all, there are a few ways you can actually collect and pass params to a test:

### Run a test with its defaults
`cd example`
`pytest test_example.py ...`

### Run a test with a specific scenario
`cd example`
`pytest test_example.py --scenario_file=example_scenarios.YAML --scenario_name=example_scenario_1 ...`

### Loop through a collection of scenarios that match some tag
`cd example`
`pytest example_scenarios.YAML --tags=smoke ...`

- As shown above, this case involves passing a `.YAML` scenario file into pytest instead of a `.py` file.
- The `--scenario_file` and `--scenario_name` args will be ignored in this case
- The `--tags` arg supports AND/OR operators according to the opensource `tag-matcher` package. More info on these operators can be found [here](https://pypi.org/project/tag-matcher/).
- If no `--tags` arg is specified, then ALL the scenarios in the `.yaml` file will be run.
- If a value is given for `--scenario_delay`, the test will wait that many seconds between collected scenarios.
- All scenarios are implicitly tagged with the scenario name to support easily selecting one single
scenario

<p align="right">(<a href="#top">back to top</a>)</p>

## Configuring Grasshopper
<a id="creating"></a>

Grasshopper adds a variety of parameters relating to performance testing along with a
variety of ways to set these values.

Recent changes (>= 1.1.1) include an expanded set of sources, almost full access to all 
arguments via every source (some exceptions outlined below), and the addition of some 
new values that will be used with integrations such as report portal & slack 
(integrations are NYI). These changes are made in a backwards compatible manner, 
meaning all existing grasshopper tests should still run without modification. The 
original fixtures and sources for configuration are deprecated, but still produce the 
same behavior.

All of the usual [pytest arguments](https://docs.pytest.org/en/6.2.x/usage.html) also remain available.

The rest of the sections on configuration assume you are using `locust-grasshopper>=1.1.1`.
<p align="right">(<a href="#top">back to top</a>)</p>

### Sources
Currently, there are 5 different sources for configuration values and they are, in 
precedence order 

+ command line arguments
+ environment variables
+ scenario values from a scenario yaml file
+ grasshopper configuration file
+ global defaults (currently stored in code, not configurable by consumers)

Obviously, the global defaults defined by Grasshopper are not really a source for
consumers to modify, but we mention it so values don't seem to appear "out of thin air".
<p align="right">(<a href="#top">back to top</a>)</p>

### Categories
The argument list is getting lengthy, so we've broken it down into categories. These
categories are entirely for humans: better readability, understanding and ease of use. 
Once they are all fully loaded by Grasshopper, they will be stored in a single 
`GHConfiguration` object (`dict`). By definition, every argument is in only one category
and there is no overlap of keys between the categories. If the same key is supplied in
multiple categories, they will be merged with the precedence order as they appear in
the table.

| Name        | Scope              | Description/Usage                                                                                                                                                                                                                                         |
|-------------| ------------------ |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Grasshopper | Session            | Variables that rarely change, may span many test runs.                                                                                                                                                                                                    |
| Test Run    | Session            | Variables that may change per test run, but are the<br/>same for every scenario in the run                                                                                                                                                                |
| Scenario    | Session            | Variables that may change per scenario and are often<br/>scenario specific; Includes user defined variables that are<br/>not declared as command line arguments by Grasshopper.<br/>However, you may use pytest's addoptions hook in your <br/>conftest to define them. |

At least one of the sections must be present in the global configuration file and
eventually this will be the same in the configuration section of a scenario in a scenario 
yaml file. Categories are not used when specifying environment variables or command line
options. We recommend that you use these categories in file sources, but if a variable 
is in the wrong section, it won't actually affect the configuration loading process.
<p align="right">(<a href="#top">back to top</a>)</p>

### Using Configuration Values 
Your test(s) may access the complete merged set of key-value pairs via the session scoped 
fixture `complete_configuration`. This returns a `GHConfiguration` object (dict) which
is unique to the current scenario. This value will be re-calculated for each new scenario
executed.

A few perhaps not obvious notes about configuration:
+ use the environment variable convention of all uppercase key names (e.g. `RUNTIME=10`)
to _specify_ a key-value pair via an environment value
+ use the lower case key to _access_ a key from the `GHConfiguration` object 
(e.g. `x = complete_configuration("runtime")`) regardless of the original source(s)
+ use `--` before the key name to specify it on the command line (e.g. `--runtime=10`)
+ configure a grasshopper configuration file by creating a session scoped fixture loaded 
by your conftest.py called `grasshopper_config_file_path` which returns the full path to a 
configuration yaml file.
+ grasshopper supports thresholds specified as
  + a json string - required for environment variable or commandline, but also accepted
  from other sources
  + a dict - when passing in via the `scenario_args` method (more details on that below)
  or via a journey class's `defaults` attr.

```python
@pytest.fixture(scope="session")
def grasshopper_config_file_path():
    return "path/to/your/config/file"
```

An example grasshopper configuration file:
```yaml
grasshopper:
  influx_host: 1.1.1.1
test_run:
  users: 1.0
  spawn_rate: 1.0
  runtime: 600
scenario:
  key1 : 'value1'
  key2: 0
```
<p align="right">(<a href="#top">back to top</a>)</p>

### Additional Extensions to the configuration loading process

If you would like to include other environment variables that might be present in the
system, you can define a fixture called `extra_env_var_keys` which returns a list of key
names to load from the `os.environ`. Keys that are missing in the environment will not 
be included in the `GHConfiguration` object.

Any environment variables that use the prefix `GH_` will also be included in the 
`GHConfiguration` object. The `GH_` will be stripped before adding and any names that
become zero length after the strip will be discarded. This is a mechanism to include any
scenario arguments you might like to pass via an environment variable.

In the unlikely case that you need to use a different prefix to designate scenario
variables, you can define a fixture called `env_var_prefix_key` which returns a prefix
string. The same rules apply about which values are included in the configuration.

<p align="right">(<a href="#top">back to top</a>)</p>

## Checks
<a id="checks"></a>
Checks are an assertion that is recorded as a metric. 
They are useful both to ensure your test is working correctly 
(e.g. are you getting a valid id back from some post that you sent) 
and to evaluate if the load is causing intermittent failures 
(e.g. sometimes a percentage of workflow runs don't complete correctly the load increases). 
At the end of the test, checks are aggregated by their name across all journeys that 
ran and then reported to the console. They are also forwarded to the DB 
in the "checks" table. Here is an example of using a check: 

```python
from grasshopper.lib.util.utils import check
...
response = self.client.get(
    'https://google.com', name='get google'
)
check(
    "get google responded with a 200",
    response.status_code == 200,
    env=self.environment,
)
```
It is worth noting that it is NOT necessary to add checks on the http codes. All the 
HTTP return codes are tracked automatically by grasshopper and will be sent to the DB. 
If you aren't using a DB then you might want the checks console output.
<p align="right">(<a href="#top">back to top</a>)</p>

## Custom Trends
<a id="custom-trends"></a>
Custom trends are useful when you want to time something that spans multiple HTTP 
calls. They are reported to the specified database just like any other HTTP request, 
but with the "CUSTOM" HTTP verb as opposed to "GET", "POST", etc. Here is an example 
of using a custom trend:
```python
from locust import between, task
from grasshopper.lib.util.utils import custom_trend
...

@task
@custom_trend("my_custom_trend")
def google_get_journey(self)
    for i in range(len(10)):
        response = self.client.get(
            'https://google.com', name='get google', context={'foo1':'bar1'}
        )
```
<p align="right">(<a href="#top">back to top</a>)</p>

## Thresholds
<a id="thresholds"></a>
Thresholds are time-based, and can be added to any trend, whether it be a custom 
trend or a request response time. Thresholds default to the 0.9 percentile of timings. 
Here is an example of using a threshold: 

```python
# a journey class with an example threshold
from locust import between, task
from grasshopper.lib.journeys.base_journey import BaseJourney
from grasshopper.lib.grasshopper import Grasshopper

class ExampleJourney(BaseJourney):
    # number of seconds to wait between each task
    wait_time = between(min_wait=20, max_wait=30)
    
    @task
    def example_task:
        self.client.get("https://google.com", name="get google")
        
    @task
    @custom_trend("my custom trend")
    def example_task_custom_trend:
        time.sleep(10)

# the pytest test which launches the journey class, thresholds could be 
# parameterized here as well.
def test_run_example_journey(complete_configuration):
    ExampleJourney.update_incoming_scenario_args(complete_configuration)
    ExampleJourney.update_incoming_scenario_args({
        "thresholds": {
            "get google":
                {
                    "type": "get",
                    "limit": 4000  # 4 second HTTP response threshold
                },
            "my custom trend":
                {
                    "type": "custom",
                    "limit": 11000  # 11 second custom trend threshold
                }
        }
    })
    
    locust_env = Grasshopper.launch_test(ExampleJourney, **complete_configuration)
    return locust_env
```

Thresholds can also be defined for individual YAML scenarios. Refer to the `thresholds` 
key in `example/example_scenarios.YAML` for how to use thresholds for YAML scenarios.

After a test has concluded, trend/threshold data can be found in 
`locust_env.stats.trends`. 
This data is also reported to the console at the end of each test.

<p align="right">(<a href="#top">back to top</a>)</p>

## Time Series DB Reporting and Tagging
<a id="db-reporting"></a>

Additional design details about how a database listener works with grasshopper/locust can be 
found in the [Database Listener Design Documentation](./docs/database_listener_design_documentation.md).

When you specify a time series database URL param to `launch_test`, such as 
`influx_host`, all metrics will be automatically reported to tables within the `locust` 
timeseries database via the specified URL. These tables include:
- `locust_checks`: check name, check passed, etc.
- `locust_events`: test started, test stopped, etc.
- `locust_exceptions`: error messages
- `locust_requests`: HTTP requests and custom trends

To run the influxdb/grafana locally, you can use the docker-compose file in the example directory:
```shell
cd example/observability_infrastructure
docker-compose up -d
```
and then you can access the grafana UI at `localhost`. The default username/password is `admin/admin`.
To then run a test which reports to this influxdb just add the `--influx_host=localhost` handle. 


There are a few ways you can pass in extra tags which 
will be reported to the time series DB:

1. **HTTP Request Tagging**   
     All HTTP requests are automatically tagged with their name. If you want to pass in 
     extra tags for a particular HTTP request, you can pass them in 
     as a dictionary for the `context` param when making a request. For example:

    ```python
    self.client.get('https://google.com', name='get google', context={'foo':'bar'})
    ```
    The tags on this metric would then be: `{'name': 'get google', 'foo': 'bar'}` which 
    would get forwarded to the database if specified. 

2. **Check Tagging**   
   When defining a check, you can pass in extra tags with the `tags` parameter:
    ```python
    from grasshopper.lib.util.utils import check
    ...
    response = self.client.get(
    'https://google.com', name='get google', context={'foo1':'bar1'}
    )
    check(
       "get google responded with a 200",
       response.status_code == 200,
       env=self.environment,
       tags = {'foo2': 'bar2'}
    )
    ```

3. **Custom Trend Tagging**   
    Since custom trends are decorators, they do not have access to 
   non-static class variables when defined. Therefore, you must use the 
   `extra_tag_keys` param, which is an array of keys that exist in the journey's 
   scenario_args. So for example, if a journey had the scenario args `{"foo" : "bar"}` and you wanted to tag 
   a custom trend based on the "foo" scenario arg key, you would do something like this:
    ```python
    from locust import between, task
    from grasshopper.lib.util.utils import custom_trend
    ...
        
    @task
    @custom_trend("my_custom_trend", extra_tag_keys=["foo"])
    def google_get_journey(self)
       for i in range(len(10)):
          response = self.client.get(
             'https://google.com', name='get google', context={'foo1':'bar1'}
          )
    ```
<p align="right">(<a href="#top">back to top</a>)</p>

## Project Roadmap

- [X] Custom Trends
- [X] Checks
- [X] Thresholds
- [X] Tagging
- [X] InfluxDB metric reporting
- [X] docker-compose template for influxdb/grafana
- [ ] PrometheusDB metric reporting
- [ ] Slack reporting
- [ ] ReportPortal reporting

See the open issues for a full list of proposed features (and known issues).

<p align="right">(<a href="#top">back to top</a>)</p>

## Contributing

Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.

If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
Don't forget to give the project a star! Thanks again!

1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Make sure unit tests pass (`pytest tests/unit`)
4. Add unit tests to keep coverage up, if necessary
5. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
6. Push to the Branch (`git push origin feature/AmazingFeature`)
7. Open a Pull Request

<p align="right">(<a href="#top">back to top</a>)</p>

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "locust-grasshopper",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4,>=3.10",
    "maintainer_email": "\"Alteryx, Inc.\" <open_source_support@alteryx.com>",
    "keywords": "load testing, performance, locust, grasshopper",
    "author": null,
    "author_email": "\"Alteryx, Inc.\" <open_source_support@alteryx.com>",
    "download_url": "https://files.pythonhosted.org/packages/cc/08/d6e9913a92e1f47ac32959950b6227772d5e014c5f861a09073305f653af/locust_grasshopper-1.3.27.tar.gz",
    "platform": null,
    "description": "<div id=\"top\"></div>\n\n# Grasshopper\n\nA lightweight framework for performing load tests against an environment, primarily \nagainst an API. Grasshopper glues [Locust](https://locust.io/), [Pytest](https://docs.pytest.org/en/7.1.x/#), some plugins (namely [Locust InfluxDBListener](https://github.com/hoodoo-digital/locust-influxdb-listener) ) and some custom code to provide a\npackage that makes authoring load tests simple with very little boilerplate needed.\n\nHere are some key functionalities that this project extends on Locust:\n- [checks](#checks)\n- [custom trends](#custom-trends)\n- [timing thresholds](#thresholds)\n- [streamlined metric reporting/tagging system](#db-reporting)\n  (only influxDB is supported right now)\n\n## Installation\nThis package can be installed via pip: `pip install locust-grasshopper`\n\n## Example Load Test\n- You can refer to the test `test_example.py` in the `example` directory for a basic \n  skeleton of how to get a load test running. In the same directory, there is also an \n  example `conftest.py` that will show you how to get basic parameterization working.\n- This test can be invoked by running `pytest example/test_example.py` in the root of \n  this project.\n- This test can also be invoked via a YAML scenario file:\n```shell\ncd example\npytest example_scenarios.YAML --tags=example1\n```\n In this example scenario file, you can see how grasshopper_args, \n grasshopper_scenario_args, and tags are being set.\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Creating a load test\nWhen creating a new load test, the primary grasshopper function you will be using \nis called `Grasshopper.launch_test`. This function can be imported like so: `from grasshopper.lib.grasshopper import Grasshopper`\n`launch_test` takes in a wide variety of args:\n- `user_classes`: User classes that the runner will run. These user classes must \n  extend BaseJourney, which is a grasshopper class \n  (`from grasshopper.lib.journeys.base_journey import BaseJourney`). This can be a \n  single class, a list of classes, or a dictionary where the key is the class and \n  the value is the locust weight to assign to that class.\n- `**complete_configuration`: In order for the test to have the correct configuration, you \n  must pass in the kwargs provided by the `complete_configuration` fixture. See example \n  load test on how to do this properly.\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Scenario Args   \n\n- If you want to parameterize your journey class, you should use the `scenario_args` \n  dict. This is the proper way to pass in values from outside of \n  the journey for access by the journey code. Note that each journey gets a \n  ***copy*** on start, so the journey itself can safely modify its own dictionary \n  once the test is running.\n  `scenario_args` exists for any journey that extends the grasshopper `base_journey` \n  class. \n  `scenario_args` also grabs from `self.defaults` on initialization. For example:\n```python\nfrom locust import between, task\nfrom grasshopper.lib.journeys.base_journey import BaseJourney\nfrom grasshopper.lib.grasshopper import Grasshopper\n\n# a journey class with an example task\nclass ExampleJourney(BaseJourney):\n    # number of seconds to wait between each task\n    wait_time = between(min_wait=20, max_wait=30)\n    \n    # this defaults dictionary will be merged into scenario_args with lower precedence \n    # when the journey is initialized\n    defaults = {\n        \"foo\": \"bar\",\n    }\n    \n    @task\n    def example_task:\n        logging.info(f'foo is `{self.scenario_args.get(\"foo\")}`.')\n        \n        # aggregate all metrics for the below request under the name \"get google\"\n        # if name is not specified, then the full url will be the name of the metric\n        response = self.client.get('https://google.com', name='get google')\n\n# the pytest test which launches the journey class\ndef test_run_example_journey(complete_configuration):\n    # update scenario args before initialization\n    ExampleJourney.update_incoming_scenario_args(complete_configuration)\n    \n    # launch the journey\n    locust_env = Grasshopper.launch_test(ExampleJourney, **complete_configuration)\n    return locust_env\n```\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Commonly used grasshopper pytest arguments\n- `--runtime`: Number of seconds to run each test. Set to 120 by default.\n- `--users`: Max number of users that are spawned. Set to 1 by default.\n- `--spawn_rate` : Number of users to spawn per second. Set to 1 by default.\n- `--shape`: The name of a shape to run for the test. \nIf you don't specify a shape or shape instance, then the shape `Default` will be used, \n  which just runs with the users, runtime & spawn_rate specified on the command line (or picks up defaults \nof 1, 1, 120s). See `utils/shapes.py` for available shapes and information on how to add\nyour own shapes.\n- `--scenario_file` If you want a yaml file where you pre-define some args, this is how \nyou specify that file path. For example, \n  `scenario_file=example/scenario_example.YAML`. \n- `--scenario_name` If `--scenario_file` was specified, this is the scenario name that is \nwithin that YAML file that corresponds to the scenario you wish to run. Defaults to None.\n- `--tags` See below example: `Loop through a collection of scenarios that match some \n  tag`.\n- `--scenario_delay` Adds a delay in seconds between scenarios. Defaults to 0.\n- `--influx_host` If you want to report your performance test metrics to some influxdb, \nyou must specify a host.\n    E.g. `1.1.1.1`. Defaults to None.\n- `--influx_port`: Port for your `influx_host` in the case where it is non-default.\n- `--influx_ssl`: If your influxdb is using SSL, set this to True. Defaults to False.\n- `--influx_verify_ssl`: If your influxdb is using SSL, set this to True. Defaults to \n    False.\n- `--influx_user`: Username for your `influx_host`, if you have one.\n- `--influx_pwd`: Password for your `influx_host`, if you have one.\n- `--grafana_host`: If your grafana is a separate URL from the influxdb, you can \n  specify it here. If you don't, then the grafana URL will be the same as the \n  influxdb URL when the grasshopper object generates grafana links. \n\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Launching tests with a configuration\nAll in all, there are a few ways you can actually collect and pass params to a test:\n\n### Run a test with its defaults\n`cd example`\n`pytest test_example.py ...`\n\n### Run a test with a specific scenario\n`cd example`\n`pytest test_example.py --scenario_file=example_scenarios.YAML --scenario_name=example_scenario_1 ...`\n\n### Loop through a collection of scenarios that match some tag\n`cd example`\n`pytest example_scenarios.YAML --tags=smoke ...`\n\n- As shown above, this case involves passing a `.YAML` scenario file into pytest instead of a `.py` file.\n- The `--scenario_file` and `--scenario_name` args will be ignored in this case\n- The `--tags` arg supports AND/OR operators according to the opensource `tag-matcher` package. More info on these operators can be found [here](https://pypi.org/project/tag-matcher/).\n- If no `--tags` arg is specified, then ALL the scenarios in the `.yaml` file will be run.\n- If a value is given for `--scenario_delay`, the test will wait that many seconds between collected scenarios.\n- All scenarios are implicitly tagged with the scenario name to support easily selecting one single\nscenario\n\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Configuring Grasshopper\n<a id=\"creating\"></a>\n\nGrasshopper adds a variety of parameters relating to performance testing along with a\nvariety of ways to set these values.\n\nRecent changes (>= 1.1.1) include an expanded set of sources, almost full access to all \narguments via every source (some exceptions outlined below), and the addition of some \nnew values that will be used with integrations such as report portal & slack \n(integrations are NYI). These changes are made in a backwards compatible manner, \nmeaning all existing grasshopper tests should still run without modification. The \noriginal fixtures and sources for configuration are deprecated, but still produce the \nsame behavior.\n\nAll of the usual [pytest arguments](https://docs.pytest.org/en/6.2.x/usage.html) also remain available.\n\nThe rest of the sections on configuration assume you are using `locust-grasshopper>=1.1.1`.\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n### Sources\nCurrently, there are 5 different sources for configuration values and they are, in \nprecedence order \n\n+ command line arguments\n+ environment variables\n+ scenario values from a scenario yaml file\n+ grasshopper configuration file\n+ global defaults (currently stored in code, not configurable by consumers)\n\nObviously, the global defaults defined by Grasshopper are not really a source for\nconsumers to modify, but we mention it so values don't seem to appear \"out of thin air\".\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n### Categories\nThe argument list is getting lengthy, so we've broken it down into categories. These\ncategories are entirely for humans: better readability, understanding and ease of use. \nOnce they are all fully loaded by Grasshopper, they will be stored in a single \n`GHConfiguration` object (`dict`). By definition, every argument is in only one category\nand there is no overlap of keys between the categories. If the same key is supplied in\nmultiple categories, they will be merged with the precedence order as they appear in\nthe table.\n\n| Name        | Scope              | Description/Usage                                                                                                                                                                                                                                         |\n|-------------| ------------------ |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| Grasshopper | Session            | Variables that rarely change, may span many test runs.                                                                                                                                                                                                    |\n| Test Run    | Session            | Variables that may change per test run, but are the<br/>same for every scenario in the run                                                                                                                                                                |\n| Scenario    | Session            | Variables that may change per scenario and are often<br/>scenario specific; Includes user defined variables that are<br/>not declared as command line arguments by Grasshopper.<br/>However, you may use pytest's addoptions hook in your <br/>conftest to define them. |\n\nAt least one of the sections must be present in the global configuration file and\neventually this will be the same in the configuration section of a scenario in a scenario \nyaml file. Categories are not used when specifying environment variables or command line\noptions. We recommend that you use these categories in file sources, but if a variable \nis in the wrong section, it won't actually affect the configuration loading process.\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n### Using Configuration Values \nYour test(s) may access the complete merged set of key-value pairs via the session scoped \nfixture `complete_configuration`. This returns a `GHConfiguration` object (dict) which\nis unique to the current scenario. This value will be re-calculated for each new scenario\nexecuted.\n\nA few perhaps not obvious notes about configuration:\n+ use the environment variable convention of all uppercase key names (e.g. `RUNTIME=10`)\nto _specify_ a key-value pair via an environment value\n+ use the lower case key to _access_ a key from the `GHConfiguration` object \n(e.g. `x = complete_configuration(\"runtime\")`) regardless of the original source(s)\n+ use `--` before the key name to specify it on the command line (e.g. `--runtime=10`)\n+ configure a grasshopper configuration file by creating a session scoped fixture loaded \nby your conftest.py called `grasshopper_config_file_path` which returns the full path to a \nconfiguration yaml file.\n+ grasshopper supports thresholds specified as\n  + a json string - required for environment variable or commandline, but also accepted\n  from other sources\n  + a dict - when passing in via the `scenario_args` method (more details on that below)\n  or via a journey class's `defaults` attr.\n\n```python\n@pytest.fixture(scope=\"session\")\ndef grasshopper_config_file_path():\n    return \"path/to/your/config/file\"\n```\n\nAn example grasshopper configuration file:\n```yaml\ngrasshopper:\n  influx_host: 1.1.1.1\ntest_run:\n  users: 1.0\n  spawn_rate: 1.0\n  runtime: 600\nscenario:\n  key1 : 'value1'\n  key2: 0\n```\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n### Additional Extensions to the configuration loading process\n\nIf you would like to include other environment variables that might be present in the\nsystem, you can define a fixture called `extra_env_var_keys` which returns a list of key\nnames to load from the `os.environ`. Keys that are missing in the environment will not \nbe included in the `GHConfiguration` object.\n\nAny environment variables that use the prefix `GH_` will also be included in the \n`GHConfiguration` object. The `GH_` will be stripped before adding and any names that\nbecome zero length after the strip will be discarded. This is a mechanism to include any\nscenario arguments you might like to pass via an environment variable.\n\nIn the unlikely case that you need to use a different prefix to designate scenario\nvariables, you can define a fixture called `env_var_prefix_key` which returns a prefix\nstring. The same rules apply about which values are included in the configuration.\n\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Checks\n<a id=\"checks\"></a>\nChecks are an assertion that is recorded as a metric. \nThey are useful both to ensure your test is working correctly \n(e.g. are you getting a valid id back from some post that you sent) \nand to evaluate if the load is causing intermittent failures \n(e.g. sometimes a percentage of workflow runs don't complete correctly the load increases). \nAt the end of the test, checks are aggregated by their name across all journeys that \nran and then reported to the console. They are also forwarded to the DB \nin the \"checks\" table. Here is an example of using a check: \n\n```python\nfrom grasshopper.lib.util.utils import check\n...\nresponse = self.client.get(\n    'https://google.com', name='get google'\n)\ncheck(\n    \"get google responded with a 200\",\n    response.status_code == 200,\n    env=self.environment,\n)\n```\nIt is worth noting that it is NOT necessary to add checks on the http codes. All the \nHTTP return codes are tracked automatically by grasshopper and will be sent to the DB. \nIf you aren't using a DB then you might want the checks console output.\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Custom Trends\n<a id=\"custom-trends\"></a>\nCustom trends are useful when you want to time something that spans multiple HTTP \ncalls. They are reported to the specified database just like any other HTTP request, \nbut with the \"CUSTOM\" HTTP verb as opposed to \"GET\", \"POST\", etc. Here is an example \nof using a custom trend:\n```python\nfrom locust import between, task\nfrom grasshopper.lib.util.utils import custom_trend\n...\n\n@task\n@custom_trend(\"my_custom_trend\")\ndef google_get_journey(self)\n    for i in range(len(10)):\n        response = self.client.get(\n            'https://google.com', name='get google', context={'foo1':'bar1'}\n        )\n```\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Thresholds\n<a id=\"thresholds\"></a>\nThresholds are time-based, and can be added to any trend, whether it be a custom \ntrend or a request response time. Thresholds default to the 0.9 percentile of timings. \nHere is an example of using a threshold: \n\n```python\n# a journey class with an example threshold\nfrom locust import between, task\nfrom grasshopper.lib.journeys.base_journey import BaseJourney\nfrom grasshopper.lib.grasshopper import Grasshopper\n\nclass ExampleJourney(BaseJourney):\n    # number of seconds to wait between each task\n    wait_time = between(min_wait=20, max_wait=30)\n    \n    @task\n    def example_task:\n        self.client.get(\"https://google.com\", name=\"get google\")\n        \n    @task\n    @custom_trend(\"my custom trend\")\n    def example_task_custom_trend:\n        time.sleep(10)\n\n# the pytest test which launches the journey class, thresholds could be \n# parameterized here as well.\ndef test_run_example_journey(complete_configuration):\n    ExampleJourney.update_incoming_scenario_args(complete_configuration)\n    ExampleJourney.update_incoming_scenario_args({\n        \"thresholds\": {\n            \"get google\":\n                {\n                    \"type\": \"get\",\n                    \"limit\": 4000  # 4 second HTTP response threshold\n                },\n            \"my custom trend\":\n                {\n                    \"type\": \"custom\",\n                    \"limit\": 11000  # 11 second custom trend threshold\n                }\n        }\n    })\n    \n    locust_env = Grasshopper.launch_test(ExampleJourney, **complete_configuration)\n    return locust_env\n```\n\nThresholds can also be defined for individual YAML scenarios. Refer to the `thresholds` \nkey in `example/example_scenarios.YAML` for how to use thresholds for YAML scenarios.\n\nAfter a test has concluded, trend/threshold data can be found in \n`locust_env.stats.trends`. \nThis data is also reported to the console at the end of each test.\n\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Time Series DB Reporting and Tagging\n<a id=\"db-reporting\"></a>\n\nAdditional design details about how a database listener works with grasshopper/locust can be \nfound in the [Database Listener Design Documentation](./docs/database_listener_design_documentation.md).\n\nWhen you specify a time series database URL param to `launch_test`, such as \n`influx_host`, all metrics will be automatically reported to tables within the `locust` \ntimeseries database via the specified URL. These tables include:\n- `locust_checks`: check name, check passed, etc.\n- `locust_events`: test started, test stopped, etc.\n- `locust_exceptions`: error messages\n- `locust_requests`: HTTP requests and custom trends\n\nTo run the influxdb/grafana locally, you can use the docker-compose file in the example directory:\n```shell\ncd example/observability_infrastructure\ndocker-compose up -d\n```\nand then you can access the grafana UI at `localhost`. The default username/password is `admin/admin`.\nTo then run a test which reports to this influxdb just add the `--influx_host=localhost` handle. \n\n\nThere are a few ways you can pass in extra tags which \nwill be reported to the time series DB:\n\n1. **HTTP Request Tagging**   \n     All HTTP requests are automatically tagged with their name. If you want to pass in \n     extra tags for a particular HTTP request, you can pass them in \n     as a dictionary for the `context` param when making a request. For example:\n\n    ```python\n    self.client.get('https://google.com', name='get google', context={'foo':'bar'})\n    ```\n    The tags on this metric would then be: `{'name': 'get google', 'foo': 'bar'}` which \n    would get forwarded to the database if specified. \n\n2. **Check Tagging**   \n   When defining a check, you can pass in extra tags with the `tags` parameter:\n    ```python\n    from grasshopper.lib.util.utils import check\n    ...\n    response = self.client.get(\n    'https://google.com', name='get google', context={'foo1':'bar1'}\n    )\n    check(\n       \"get google responded with a 200\",\n       response.status_code == 200,\n       env=self.environment,\n       tags = {'foo2': 'bar2'}\n    )\n    ```\n\n3. **Custom Trend Tagging**   \n    Since custom trends are decorators, they do not have access to \n   non-static class variables when defined. Therefore, you must use the \n   `extra_tag_keys` param, which is an array of keys that exist in the journey's \n   scenario_args. So for example, if a journey had the scenario args `{\"foo\" : \"bar\"}` and you wanted to tag \n   a custom trend based on the \"foo\" scenario arg key, you would do something like this:\n    ```python\n    from locust import between, task\n    from grasshopper.lib.util.utils import custom_trend\n    ...\n        \n    @task\n    @custom_trend(\"my_custom_trend\", extra_tag_keys=[\"foo\"])\n    def google_get_journey(self)\n       for i in range(len(10)):\n          response = self.client.get(\n             'https://google.com', name='get google', context={'foo1':'bar1'}\n          )\n    ```\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Project Roadmap\n\n- [X] Custom Trends\n- [X] Checks\n- [X] Thresholds\n- [X] Tagging\n- [X] InfluxDB metric reporting\n- [X] docker-compose template for influxdb/grafana\n- [ ] PrometheusDB metric reporting\n- [ ] Slack reporting\n- [ ] ReportPortal reporting\n\nSee the open issues for a full list of proposed features (and known issues).\n\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n\n## Contributing\n\nContributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.\n\nIf you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag \"enhancement\".\nDon't forget to give the project a star! Thanks again!\n\n1. Fork the Project\n2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)\n3. Make sure unit tests pass (`pytest tests/unit`)\n4. Add unit tests to keep coverage up, if necessary\n5. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)\n6. Push to the Branch (`git push origin feature/AmazingFeature`)\n7. Open a Pull Request\n\n<p align=\"right\">(<a href=\"#top\">back to top</a>)</p>\n",
    "bugtrack_url": null,
    "license": "Apache License Version 2.0, January 2004 http://www.apache.org/licenses/  TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION  1. Definitions.  \"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.  \"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.  \"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.  \"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.  \"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.  \"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.  \"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).  \"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.  \"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"  \"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.  2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.  3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.  4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:  (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and  (b) You must cause any modified files to carry prominent notices stating that You changed the files; and  (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and  (d) If the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.  You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.  5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.  6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.  7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.  8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.  9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.  END OF TERMS AND CONDITIONS  APPENDIX: How to apply the Apache License to your work.  To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets \"[]\" replaced with your own identifying information. (Don't include the brackets!)  The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \"printed page\" as the copyright notice for easier identification within third-party archives.  Copyright [yyyy] [name of copyright owner]  Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at  http://www.apache.org/licenses/LICENSE-2.0  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.",
    "summary": "a load testing tool extended from locust",
    "version": "1.3.27",
    "project_urls": {
        "repository": "https://github.com/alteryx/locust-grasshopper"
    },
    "split_keywords": [
        "load testing",
        " performance",
        " locust",
        " grasshopper"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "18bb812ef69d8d047f3305de1b38542806d280cd9dded08081ee7eee44685c1d",
                "md5": "ee4ead24e5bba6b0fb10e325230cfea0",
                "sha256": "6366e79ff8acd0606c142aa65e32d4567c5d62e7068b9de12bced73486348f35"
            },
            "downloads": -1,
            "filename": "locust_grasshopper-1.3.27-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "ee4ead24e5bba6b0fb10e325230cfea0",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4,>=3.10",
            "size": 51081,
            "upload_time": "2024-07-31T07:22:14",
            "upload_time_iso_8601": "2024-07-31T07:22:14.333733Z",
            "url": "https://files.pythonhosted.org/packages/18/bb/812ef69d8d047f3305de1b38542806d280cd9dded08081ee7eee44685c1d/locust_grasshopper-1.3.27-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "cc08d6e9913a92e1f47ac32959950b6227772d5e014c5f861a09073305f653af",
                "md5": "d908345615cd32e89759a21a4a009fb6",
                "sha256": "fb8028e29fb26dbf6bfa9a97eac81b4e9c9c282456f02291837b118aeda023d3"
            },
            "downloads": -1,
            "filename": "locust_grasshopper-1.3.27.tar.gz",
            "has_sig": false,
            "md5_digest": "d908345615cd32e89759a21a4a009fb6",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4,>=3.10",
            "size": 88522,
            "upload_time": "2024-07-31T07:22:16",
            "upload_time_iso_8601": "2024-07-31T07:22:16.577958Z",
            "url": "https://files.pythonhosted.org/packages/cc/08/d6e9913a92e1f47ac32959950b6227772d5e014c5f861a09073305f653af/locust_grasshopper-1.3.27.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-07-31 07:22:16",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "alteryx",
    "github_project": "locust-grasshopper",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "locust-grasshopper"
}
        
Elapsed time: 4.99342s