Merge pull request #943 from httprunner/v3

## 3.1.0 (2020-06-21)

**Added**

- feat: integrate [locust](https://locust.io/) v1.0

**Changed**

- change: make converted referenced pytest files always relative to ProjectRootDir
- change: log function details when call function failed
- change: do not raise error if failed to get client/server address info
This commit is contained in:
debugtalk
2020-06-21 12:40:10 +08:00
committed by GitHub
49 changed files with 1245 additions and 369 deletions

1
.gitignore vendored
View File

@@ -11,7 +11,6 @@ dist/*
.python-version
logs
.coverage
locustfile.py
site/
reports
.venv

View File

@@ -42,7 +42,7 @@ Thank you to all our sponsors! ✨🍰✨ ([become a sponsor](docs/sponsors.md))
> [霍格沃兹测试学院](https://ceshiren.com/) 是业界领先的测试开发技术高端教育品牌,隶属于测吧(北京)科技有限公司。学院课程均由 BAT 一线测试大咖执教,提供实战驱动的接口自动化测试、移动自动化测试、性能测试、持续集成与 DevOps 等技术培训,以及测试开发优秀人才内推服务。[点击学习!](https://ke.qq.com/course/254956?flowToken=1014690)
[霍格沃兹测试学院](https://ceshiren.com/) 是 HttpRunner 的首家金牌赞助商。
霍格沃兹测试学院是 HttpRunner 的首家金牌赞助商。
### 开源服务赞助商Open Source Sponsor

View File

@@ -1,5 +1,22 @@
# Release History
## 3.1.0 (2020-06-21)
**Added**
- feat: integrate [locust](https://locust.io/) v1.0
**Changed**
- change: make converted referenced pytest files always relative to ProjectRootDir
- change: log function details when call function failed
- change: do not raise error if failed to get client/server address info
**Fixed**
- fix: path handling error when har2case har file and cwd != ProjectRootDir
- fix: missing list type for request body
## 3.0.13 (2020-06-17)
**Added**

View File

@@ -24,18 +24,19 @@ $ pip3 install -U git+https://github.com/httprunner/httprunner.git@master
## Check Installation
When HttpRunner is installed, 4 commands will be added in your system.
When HttpRunner is installed, 5 commands will be added in your system.
- `httprunner`: main command, used for all functions
- `hrun`: alias for `httprunner run`, used to run YAML/JSON/pytest testcases
- `hmake`: alias for `httprunner make`, used to convert YAML/JSON testcases to pytest files
- `har2case`: alias for `httprunner har2case`, used to convert HAR to YAML/JSON testcases
- `locusts`: used to run load test with [locust][locust]
To see `HttpRunner` version:
```text
$ httprunner -V # hrun -V
3.0.10
3.1.0
```
To see available options, run:
@@ -60,5 +61,8 @@ optional arguments:
-V, --version show version
```
> Notice: `locusts` is an individual command, for the reason to monkey patch ssl at beginning to avoid RecursionError when running locust.
[PyPI]: https://pypi.python.org/pypi
[github-actions]: https://github.com/httprunner/httprunner/actions
[github-actions]: https://github.com/httprunner/httprunner/actions
[locust]: http://locust.io/

168
docs/user/run_loadtest.md Normal file
View File

@@ -0,0 +1,168 @@
# Run load test
> Integrated since HttpRunner 3.1.0
With reuse of [`Locust`][Locust], you can run load test without extra work.
```text
$ locusts -V
locust 1.0.3
```
For full usage, you can run `locusts -h` to see help, and you will find that it is the same with `locust -h`.
The only difference is the `-f/--locustfile` argument. When you specify `-f` with a YAML/JSON testcase file, it will convert to pytest first and then run load test with HttpRunner's builtin locustfile(httprunner/ext/locust/locustfile.py).
```text
$ locusts -f examples/postman_echo/request_methods/request_with_variables.yml
2020-06-18 18:14:29.877 | INFO | httprunner.make:make_testcase:317 - start to make testcase: /Users/debugtalk/MyProjects/HttpRunner-dev/HttpRunner/examples/postman_echo/request_methods/request_with_variables.yml
2020-06-18 18:14:29.878 | INFO | httprunner.make:make_testcase:390 - generated testcase: /Users/debugtalk/MyProjects/HttpRunner-dev/HttpRunner/examples/postman_echo/request_methods/request_with_variables_test.py
2020-06-18 18:14:29.878 | INFO | httprunner.make:format_pytest_with_black:154 - format pytest cases with black ...
reformatted /Users/debugtalk/MyProjects/HttpRunner-dev/HttpRunner/examples/postman_echo/request_methods/request_with_variables_test.py
All done! ✨ 🍰 ✨
1 file reformatted, 1 file left unchanged.
[2020-06-18 18:14:30,241] LeodeMacBook-Pro.local/INFO/locust.main: Starting web interface at http://:8089
[2020-06-18 18:14:30,249] LeodeMacBook-Pro.local/INFO/locust.main: Starting Locust 1.0.3
```
In this case, you can reuse all features of [`Locust`][Locust].
```text
$ locusts -h
Usage: locust [OPTIONS] [UserClass ...]
Common options:
-h, --help show this help message and exit
-f LOCUSTFILE, --locustfile LOCUSTFILE
Python module file to import, e.g. '../other.py'.
Default: locustfile
--config CONFIG Config file path
-H HOST, --host HOST Host to load test in the following format:
http://10.21.32.33
-u NUM_USERS, --users NUM_USERS
Number of concurrent Locust users. Only used together
with --headless
-r HATCH_RATE, --hatch-rate HATCH_RATE
The rate per second in which users are spawned. Only
used together with --headless
-t RUN_TIME, --run-time RUN_TIME
Stop after the specified amount of time, e.g. (300s,
20m, 3h, 1h30m, etc.). Only used together with
--headless
-l, --list Show list of possible User classes and exit
Web UI options:
--web-host WEB_HOST Host to bind the web interface to. Defaults to '*'
(all interfaces)
--web-port WEB_PORT, -P WEB_PORT
Port on which to run web host
--headless Disable the web interface, and instead start the load
test immediately. Requires -u and -t to be specified.
--web-auth WEB_AUTH Turn on Basic Auth for the web interface. Should be
supplied in the following format: username:password
--tls-cert TLS_CERT Optional path to TLS certificate to use to serve over
HTTPS
--tls-key TLS_KEY Optional path to TLS private key to use to serve over
HTTPS
Master options:
Options for running a Locust Master node when running Locust distributed. A Master node need Worker nodes that connect to it before it can run load tests.
--master Set locust to run in distributed mode with this
process as master
--master-bind-host MASTER_BIND_HOST
Interfaces (hostname, ip) that locust master should
bind to. Only used when running with --master.
Defaults to * (all available interfaces).
--master-bind-port MASTER_BIND_PORT
Port that locust master should bind to. Only used when
running with --master. Defaults to 5557.
--expect-workers EXPECT_WORKERS
How many workers master should expect to connect
before starting the test (only when --headless used).
Worker options:
Options for running a Locust Worker node when running Locust distributed.
Only the LOCUSTFILE (-f option) need to be specified when starting a Worker, since other options such as -u, -r, -t are specified on the Master node.
--worker Set locust to run in distributed mode with this
process as worker
--master-host MASTER_HOST
Host or IP address of locust master for distributed
load testing. Only used when running with --worker.
Defaults to 127.0.0.1.
--master-port MASTER_PORT
The port to connect to that is used by the locust
master for distributed load testing. Only used when
running with --worker. Defaults to 5557.
Tag options:
Locust tasks can be tagged using the @tag decorator. These options let specify which tasks to include or exclude during a test.
-T [TAG [TAG ...]], --tags [TAG [TAG ...]]
List of tags to include in the test, so only tasks
with any matching tags will be executed
-E [TAG [TAG ...]], --exclude-tags [TAG [TAG ...]]
List of tags to exclude from the test, so only tasks
with no matching tags will be executed
Request statistics options:
--csv CSV_PREFIX Store current request stats to files in CSV format.
Setting this option will generate three files:
[CSV_PREFIX]_stats.csv, [CSV_PREFIX]_stats_history.csv
and [CSV_PREFIX]_failures.csv
--csv-full-history Store each stats entry in CSV format to
_stats_history.csv file
--print-stats Print stats in the console
--only-summary Only print the summary stats
--reset-stats Reset statistics once hatching has been completed.
Should be set on both master and workers when running
in distributed mode
Logging options:
--skip-log-setup Disable Locust's logging setup. Instead, the
configuration is provided by the Locust test or Python
defaults.
--loglevel LOGLEVEL, -L LOGLEVEL
Choose between DEBUG/INFO/WARNING/ERROR/CRITICAL.
Default is INFO.
--logfile LOGFILE Path to log file. If not set, log will go to
stdout/stderr
Step load options:
--step-load Enable Step Load mode to monitor how performance
metrics varies when user load increases. Requires
--step-users and --step-time to be specified.
--step-users STEP_USERS
User count to increase by step in Step Load mode. Only
used together with --step-load
--step-time STEP_TIME
Step duration in Step Load mode, e.g. (300s, 20m, 3h,
1h30m, etc.). Only used together with --step-load
Other options:
--show-task-ratio Print table of the User classes' task execution ratio
--show-task-ratio-json
Print json data of the User classes' task execution
ratio
--version, -V Show program's version number and exit
--exit-code-on-error EXIT_CODE_ON_ERROR
Sets the process exit code to use when a test result
contain any failure or error
-s STOP_TIMEOUT, --stop-timeout STOP_TIMEOUT
Number of seconds to wait for a simulated user to
complete any executing task before exiting. Default is
to terminate immediately. This parameter only needs to
be specified for the master process when running
Locust distributed.
User classes:
UserClass Optionally specify which User classes that should be
used (available User classes can be listed with -l or
--list)
```
Enjoy!
[Locust]: http://locust.io/

View File

@@ -418,6 +418,29 @@ timestamp_six_digits = str(int(time.time() * 1000))[-6:])
In other words, all requests in one testcase will have the same `HRUN-Request-ID` prefix, and each request will have a unique `HRUN-Request-ID` suffix.
## Client & Server IP:PORT
Sometimes, logging remote server IP and port can be great helpful for trouble shooting, especially when there are multiple servers and we want to checkout which one returns error.
Since version `3.0.13`, HttpRunner will log client & server IP:Port in debug level.
```text
2020-06-18 11:32:38.366 | INFO | httprunner.runner:__run_step:278 - run step begin: post raw text >>>>>>
2020-06-18 11:32:38.687 | DEBUG | httprunner.client:request:187 - client IP: 10.90.205.63, Port: 62802
2020-06-18 11:32:38.687 | DEBUG | httprunner.client:request:195 - server IP: 34.233.204.163, Port: 443
```
as well as testcase summary.
```text
"address": {
"client_ip": "10.90.205.63",
"client_port": 62802,
"server_ip": "34.233.204.163",
"server_port": 443
},
```
## arguments for v2.x compatibility
Besides all the arguments of `pytest`, `hrun` also has several other arguments to keep compatibility with HttpRunner v2.x.

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/httpbin/basic.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: basic.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/httpbin/hooks.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: hooks.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/httpbin/load_image.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: load_image.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/httpbin/upload.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: upload.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/httpbin/validate.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: validate.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -6,6 +6,7 @@ testcases:
-
name: request with functions
testcase: request_methods/request_with_functions.yml
weight: 2
variables:
foo1: testcase_ref_bar11
expect_foo1: testcase_ref_bar11
@@ -13,6 +14,7 @@ testcases:
-
name: request with referenced testcase
testcase: request_methods/request_with_testcase_reference.yml
weight: 3
variables:
foo1: testcase_ref_bar12
expect_foo1: testcase_ref_bar12

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
@@ -18,6 +18,7 @@ class TestCaseRequestWithFunctions(HttpRunner):
.base_url("https://postman-echo.com")
.verify(False)
.export(*["foo3"])
.locust_weight(2)
)
teststeps = [

View File

@@ -1,14 +1,14 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: request_methods/request_with_testcase_reference.yml
import os
import sys
from pathlib import Path
sys.path.insert(0, os.getcwd())
sys.path.insert(0, str(Path(__file__).parent.parent))
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
from examples.postman_echo.request_methods.request_with_functions_test import (
from request_methods.request_with_functions_test import (
TestCaseRequestWithFunctions as RequestWithFunctions,
)
@@ -26,6 +26,7 @@ class TestCaseRequestWithTestcaseReference(HttpRunner):
)
.base_url("https://postman-echo.com")
.verify(False)
.locust_weight(3)
)
teststeps = [

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/postman_echo/request_methods/hardcode.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: request_methods/hardcode.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -7,6 +7,7 @@ config:
expect_foo2: config_bar2
base_url: "https://postman-echo.com"
verify: False
weight: 2
export: ["foo3"]
teststeps:

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: request_methods/request_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
@@ -18,6 +18,7 @@ class TestCaseRequestWithFunctions(HttpRunner):
.base_url("https://postman-echo.com")
.verify(False)
.export(*["foo3"])
.locust_weight(2)
)
teststeps = [

View File

@@ -1,14 +1,14 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: request_methods/request_with_testcase_reference.yml
import os
import sys
from pathlib import Path
sys.path.insert(0, os.getcwd())
sys.path.insert(0, str(Path(__file__).parent.parent))
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
from examples.postman_echo.request_methods.request_with_functions_test import (
from request_methods.request_with_functions_test import (
TestCaseRequestWithFunctions as RequestWithFunctions,
)

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/postman_echo/request_methods/request_with_variables.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: request_methods/request_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/postman_echo/request_methods/validate_with_functions.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: request_methods/validate_with_functions.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,5 +1,5 @@
# NOTE: Generated By HttpRunner v3.0.13
# FROM: examples/postman_echo/request_methods/validate_with_variables.yml
# NOTE: Generated By HttpRunner v3.1.0
# FROM: request_methods/validate_with_variables.yml
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase

View File

@@ -1,6 +1,8 @@
__version__ = "3.0.13"
__version__ = "3.1.0"
__description__ = "One-stop solution for HTTP(S) testing."
# import firstly for monkey patch if needed
from httprunner.ext.locust import main_locusts
from httprunner.runner import HttpRunner
from httprunner.testcase import Config, Step, RunRequest, RunTestCase

View File

@@ -12,7 +12,6 @@ from requests.exceptions import (
RequestException,
)
from httprunner.exceptions import NetworkFailure
from httprunner.models import RequestData, ResponseData
from httprunner.models import SessionData, ReqRespData
from httprunner.utils import lower_dict_keys, omit_long_data
@@ -186,7 +185,7 @@ class HttpSession(requests.Session):
self.data.address.client_port = client_port
logger.debug(f"client IP: {client_ip}, Port: {client_port}")
except AttributeError as ex:
raise NetworkFailure(f"failed to get client address info: {ex}")
logger.warning(f"failed to get client address info: {ex}")
try:
server_ip, server_port = response.raw.connection.sock.getpeername()
@@ -194,7 +193,7 @@ class HttpSession(requests.Session):
self.data.address.server_port = server_port
logger.debug(f"server IP: {server_ip}, Port: {server_port}")
except AttributeError as ex:
raise NetworkFailure(f"failed to get server address info: {ex}")
logger.warning(f"failed to get server address info: {ex}")
# get length of the response content
content_size = int(dict(response.headers).get("content-length") or 0)

View File

@@ -8,9 +8,9 @@ from typing import List, Dict, Text, Union, Any
from loguru import logger
from httprunner import exceptions
from httprunner.loader import load_project_meta
from httprunner.loader import load_project_meta, convert_relative_project_root_dir
from httprunner.parser import parse_data
from httprunner.utils import sort_dict_by_custom_order, ensure_file_path_valid
from httprunner.utils import sort_dict_by_custom_order
def convert_variables(
@@ -46,7 +46,7 @@ def convert_variables(
)
def convert_jmespath(raw: Text) -> Text:
def _convert_jmespath(raw: Text) -> Text:
if not isinstance(raw, Text):
raise exceptions.TestCaseFormatError(f"Invalid jmespath extractor: {raw}")
@@ -78,7 +78,7 @@ def convert_jmespath(raw: Text) -> Text:
return ".".join(raw_list)
def convert_extractors(extractors: Union[List, Dict]) -> Dict:
def _convert_extractors(extractors: Union[List, Dict]) -> Dict:
""" convert extract list(v2) to dict(v3)
Args:
@@ -106,26 +106,26 @@ def convert_extractors(extractors: Union[List, Dict]) -> Dict:
sys.exit(1)
for k, v in v3_extractors.items():
v3_extractors[k] = convert_jmespath(v)
v3_extractors[k] = _convert_jmespath(v)
return v3_extractors
def convert_validators(validators: List) -> List:
def _convert_validators(validators: List) -> List:
for v in validators:
if "check" in v and "expect" in v:
# format1: {"check": "content.abc", "assert": "eq", "expect": 201}
v["check"] = convert_jmespath(v["check"])
v["check"] = _convert_jmespath(v["check"])
elif len(v) == 1:
# format2: {'eq': ['status_code', 201]}
comparator = list(v.keys())[0]
v[comparator][0] = convert_jmespath(v[comparator][0])
v[comparator][0] = _convert_jmespath(v[comparator][0])
return validators
def sort_request_by_custom_order(request: Dict) -> Dict:
def _sort_request_by_custom_order(request: Dict) -> Dict:
custom_order = [
"method",
"url",
@@ -146,7 +146,7 @@ def sort_request_by_custom_order(request: Dict) -> Dict:
return sort_dict_by_custom_order(request, custom_order)
def sort_step_by_custom_order(step: Dict) -> Dict:
def _sort_step_by_custom_order(step: Dict) -> Dict:
custom_order = [
"name",
"variables",
@@ -161,7 +161,7 @@ def sort_step_by_custom_order(step: Dict) -> Dict:
return sort_dict_by_custom_order(step, custom_order)
def ensure_step_attachment(step: Dict) -> Dict:
def _ensure_step_attachment(step: Dict) -> Dict:
test_dict = {
"name": step["name"],
}
@@ -176,13 +176,13 @@ def ensure_step_attachment(step: Dict) -> Dict:
test_dict["teardown_hooks"] = step["teardown_hooks"]
if "extract" in step:
test_dict["extract"] = convert_extractors(step["extract"])
test_dict["extract"] = _convert_extractors(step["extract"])
if "export" in step:
test_dict["export"] = step["export"]
if "validate" in step:
test_dict["validate"] = convert_validators(step["validate"])
test_dict["validate"] = _convert_validators(step["validate"])
if "validate_script" in step:
test_dict["validate_script"] = step["validate_script"]
@@ -191,12 +191,14 @@ def ensure_step_attachment(step: Dict) -> Dict:
def ensure_testcase_v3_api(api_content: Dict) -> Dict:
teststep = {
"request": api_content["request"],
}
teststep.update(ensure_step_attachment(api_content))
logger.info("convert api in v2 to testcase format v3")
teststep = sort_step_by_custom_order(teststep)
teststep = {
"request": _sort_request_by_custom_order(api_content["request"]),
}
teststep.update(_ensure_step_attachment(api_content))
teststep = _sort_step_by_custom_order(teststep)
return {
"config": {"name": api_content["name"]},
@@ -205,13 +207,25 @@ def ensure_testcase_v3_api(api_content: Dict) -> Dict:
def ensure_testcase_v3(test_content: Dict) -> Dict:
logger.info("ensure compatibility with testcase format v2")
v3_content = {"config": test_content["config"], "teststeps": []}
if "teststeps" not in test_content:
logger.error(f"Miss teststeps: {test_content}")
sys.exit(1)
if not isinstance(test_content["teststeps"], list):
logger.error(
f'teststeps should be list type, got {type(test_content["teststeps"])}: {test_content["teststeps"]}'
)
sys.exit(1)
for step in test_content["teststeps"]:
teststep = {}
if "request" in step:
teststep["request"] = step.pop("request")
teststep["request"] = _sort_request_by_custom_order(step.pop("request"))
elif "api" in step:
teststep["testcase"] = step.pop("api")
elif "testcase" in step:
@@ -219,9 +233,9 @@ def ensure_testcase_v3(test_content: Dict) -> Dict:
else:
raise exceptions.TestCaseFormatError(f"Invalid teststep: {step}")
teststep.update(ensure_step_attachment(step))
teststep.update(_ensure_step_attachment(step))
teststep = sort_step_by_custom_order(teststep)
teststep = _sort_step_by_custom_order(teststep)
v3_content["teststeps"].append(teststep)
return v3_content
@@ -232,23 +246,28 @@ def ensure_cli_args(args: List) -> List:
"""
# remove deprecated --failfast
if "--failfast" in args:
logger.warning(f"remove deprecated argument: --failfast")
args.pop(args.index("--failfast"))
# convert --report-file to --html
if "--report-file" in args:
logger.warning(f"replace deprecated argument --report-file with --html")
index = args.index("--report-file")
args[index] = "--html"
args.append("--self-contained-html")
# keep compatibility with --save-tests in v2
if "--save-tests" in args:
logger.warning(
f"generate conftest.py keep compatibility with --save-tests in v2"
)
args.pop(args.index("--save-tests"))
generate_conftest_for_summary(args)
_generate_conftest_for_summary(args)
return args
def generate_conftest_for_summary(args: List):
def _generate_conftest_for_summary(args: List):
for arg in args:
if os.path.exists(arg):
@@ -259,9 +278,6 @@ def generate_conftest_for_summary(args: List):
logger.error(f"No valid test path specified! \nargs: {args}")
sys.exit(1)
project_meta = load_project_meta(test_path)
project_root_dir = ensure_file_path_valid(project_meta.RootDir)
conftest_path = os.path.join(project_root_dir, "conftest.py")
conftest_content = '''# NOTICE: Generated By HttpRunner.
import json
import os
@@ -328,9 +344,13 @@ def session_fixture(request):
'''
project_meta = load_project_meta(test_path)
project_root_dir = project_meta.RootDir
conftest_path = os.path.join(project_root_dir, "conftest.py")
test_path = os.path.abspath(test_path)
logs_dir_path = os.path.join(project_root_dir, "logs")
test_path_relative_path = test_path[len(project_root_dir) + 1 :]
test_path_relative_path = convert_relative_project_root_dir(test_path)
if os.path.isdir(test_path):
file_foder_path = os.path.join(logs_dir_path, test_path_relative_path)

View File

@@ -27,10 +27,6 @@ class TeardownHooksFailure(MyBaseFailure):
pass
class NetworkFailure(MyBaseFailure):
pass
""" error type exceptions
these exceptions will mark test as error
"""

View File

@@ -8,14 +8,9 @@ Usage:
$ hrun har2case demo.har -2y
"""
import os
import sys
from loguru import logger
from sentry_sdk import capture_message
from httprunner.compat import ensure_path_sep
from httprunner.ext.har2case.core import HarParser
from sentry_sdk import capture_message
def init_har2case_parser(subparsers):
@@ -56,14 +51,6 @@ def init_har2case_parser(subparsers):
def main_har2case(args):
har_source_file = args.har_source_file
if not har_source_file or not har_source_file.endswith(".har"):
logger.error("HAR file not specified.")
sys.exit(1)
har_source_file = ensure_path_sep(har_source_file)
if not os.path.isfile(har_source_file):
logger.error(f"HAR file not exists: {har_source_file}")
sys.exit(1)
if args.to_yaml:
output_file_type = "YAML"

View File

@@ -3,7 +3,9 @@ import json
import os
import sys
import urllib.parse as urlparse
from typing import Text
from httprunner.compat import ensure_path_sep
from loguru import logger
from sentry_sdk import capture_exception
@@ -16,9 +18,26 @@ except ImportError:
JSONDecodeError = ValueError
def ensure_file_path(path: Text) -> Text:
if not path or not path.endswith(".har"):
logger.error("HAR file not specified.")
sys.exit(1)
path = ensure_path_sep(path)
if not os.path.isfile(path):
logger.error(f"HAR file not exists: {path}")
sys.exit(1)
if not os.path.isabs(path):
path = os.path.join(os.getcwd(), path)
return path
class HarParser(object):
def __init__(self, har_file_path, filter_str=None, exclude_str=None):
self.har_file_path = har_file_path
self.har_file_path = ensure_file_path(har_file_path)
self.filter_str = filter_str
self.exclude_str = exclude_str or ""

View File

@@ -0,0 +1,115 @@
import importlib.util
import inspect
import os
import sys
from typing import List
if sys.argv[0].endswith("locusts"):
try:
# monkey patch ssl at beginning to avoid RecursionError when running locust.
from gevent import monkey
monkey.patch_ssl()
from locust import main as locust_main
except ImportError:
msg = """
Locust is not installed, install first and try again.
install with pip:
$ pip install locust
"""
print(msg)
sys.exit(1)
from loguru import logger
""" converted pytest files from YAML/JSON testcases
"""
pytest_files: List = []
def is_httprunner_testcase(item):
""" check if a variable is a HttpRunner testcase class
"""
from httprunner import HttpRunner
# TODO: skip referenced testcase
return bool(
inspect.isclass(item)
and issubclass(item, HttpRunner)
and item.__name__ != "HttpRunner"
)
def prepare_locust_tests() -> List:
""" prepare locust testcases
Returns:
list: testcase class list
"""
locust_tests = []
for pytest_file in pytest_files:
spec = importlib.util.spec_from_file_location("module.name", pytest_file)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
for name, item in vars(module).items():
if not is_httprunner_testcase(item):
continue
for _ in range(item.config.weight):
locust_tests.append(item)
return locust_tests
def main_locusts():
""" locusts entrance
"""
from httprunner.utils import init_sentry_sdk
from sentry_sdk import capture_message
init_sentry_sdk()
capture_message("start to run locusts")
# avoid print too much log details in console
logger.remove()
logger.add(sys.stderr, level="WARNING")
sys.argv[0] = "locust"
if len(sys.argv) == 1:
sys.argv.extend(["-h"])
if sys.argv[1] in ["-h", "--help", "-V", "--version"]:
locust_main.main()
def get_arg_index(*target_args):
for arg in target_args:
if arg not in sys.argv:
continue
return sys.argv.index(arg) + 1
return None
# get testcase file path
testcase_index = get_arg_index("-f", "--locustfile")
if not testcase_index:
print("Testcase file is not specified, exit 1.")
sys.exit(1)
from httprunner.make import main_make
global pytest_files
testcase_file_path = sys.argv[testcase_index]
pytest_files = main_make([testcase_file_path])
if not pytest_files:
print("No valid testcases found, exit 1.")
sys.exit(1)
sys.argv[testcase_index] = os.path.join(os.path.dirname(__file__), "locustfile.py")
locust_main.main()

View File

@@ -0,0 +1,30 @@
import random
from locust import task, HttpUser, between
from httprunner.ext.locust import prepare_locust_tests
class HttpRunnerUser(HttpUser):
host = ""
wait_time = between(5, 15)
def on_start(self):
locust_tests = prepare_locust_tests()
self.testcase_runners = [
testcase().with_session(self.client) for testcase in locust_tests
]
@task
def test_any(self):
test_runner = random.choice(self.testcase_runners)
try:
test_runner.run()
except Exception as ex:
self.environment.events.request_failure.fire(
request_type="Failed",
name=test_runner.config.name,
response_time=0,
response_length=0,
exception=ex,
)

View File

@@ -46,10 +46,9 @@ import os
import sys
from typing import Text, NoReturn
from loguru import logger
from httprunner.parser import parse_variables_mapping
from httprunner.models import TStep, FunctionsMapping
from httprunner.parser import parse_variables_mapping
from loguru import logger
try:
import filetype
@@ -146,7 +145,7 @@ def multipart_encoder(**kwargs):
# value is not absolute file path, check if it is relative file path
from httprunner.loader import load_project_meta
project_meta = load_project_meta(os.getcwd())
project_meta = load_project_meta("")
_file_path = os.path.join(project_meta.RootDir, value)
is_exists_file = os.path.isfile(_file_path)

View File

@@ -78,7 +78,6 @@ def load_testcase(testcase: Dict) -> TestCase:
testcase_obj = TestCase.parse_obj(testcase)
except ValidationError as ex:
err_msg = f"TestCase ValidationError:\nerror: {ex}\ncontent: {testcase}"
logger.error(err_msg)
raise exceptions.TestCaseFormatError(err_msg)
return testcase_obj
@@ -99,7 +98,6 @@ def load_testsuite(testsuite: Dict) -> TestSuite:
testsuite_obj = TestSuite.parse_obj(testsuite)
except ValidationError as ex:
err_msg = f"TestSuite ValidationError:\nfile: {path}\nerror: {ex}"
logger.error(err_msg)
raise exceptions.TestSuiteFormatError(err_msg)
return testsuite_obj
@@ -287,6 +285,7 @@ def locate_file(start_path: Text, file_name: Text) -> Text:
file_path = os.path.join(start_dir_path, file_name)
if os.path.isfile(file_path):
# ensure absolute
return os.path.abspath(file_path)
# system root dir
@@ -431,3 +430,23 @@ def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta:
project_meta.debugtalk_path = debugtalk_path
return project_meta
def convert_relative_project_root_dir(abs_path: Text) -> Text:
""" convert absolute path to relative path, based on project_meta.RootDir
Args:
abs_path: absolute path
Returns: relative path based on project_meta.RootDir
"""
_project_meta = load_project_meta(abs_path)
if not abs_path.startswith(_project_meta.RootDir):
raise exceptions.ParamsError(
f"failed to convert absolute path to relative path based on project_meta.RootDir\n"
f"abs_path: {abs_path}\n"
f"project_meta.RootDir: {_project_meta.RootDir}"
)
return abs_path[len(_project_meta.RootDir) + 1 :]

View File

@@ -1,7 +1,7 @@
import os
import string
import subprocess
import sys
from shutil import copyfile
from typing import Text, List, Tuple, Dict, Set, NoReturn
import jinja2
@@ -21,9 +21,10 @@ from httprunner.loader import (
load_testcase,
load_testsuite,
load_project_meta,
convert_relative_project_root_dir,
)
from httprunner.response import uniform_validator
from httprunner.utils import ensure_file_path_valid, override_config_variables
from httprunner.utils import override_config_variables
""" cache converted pytest files, avoid duplicate making
"""
@@ -36,11 +37,15 @@ pytest_files_run_set: Set = set()
__TEMPLATE__ = jinja2.Template(
"""# NOTE: Generated By HttpRunner v{{ version }}
# FROM: {{ testcase_path }}
{% if imports_list %}
import os
{% if imports_list and diff_levels > 0 %}
import sys
from pathlib import Path
sys.path.insert(0, os.getcwd())
sys.path.insert(0, str(Path(__file__)
{% for _ in range(diff_levels) %}
.parent
{% endfor %}
))
{% endif %}
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase
{% for import_str in imports_list %}
@@ -80,24 +85,51 @@ def __ensure_absolute(path: Text) -> Text:
absolute_path = os.path.join(project_meta.RootDir, path)
if not os.path.isfile(absolute_path):
raise exceptions.ParamsError(f"Invalid testcase file path: {absolute_path}")
logger.error(f"Invalid testcase file path: {absolute_path}")
sys.exit(1)
return absolute_path
def __ensure_cwd_relative(path: Text) -> Text:
""" convert absolute path to relative path, based on os.getcwd()
def ensure_file_abs_path_valid(file_abs_path: Text) -> Text:
""" ensure file path valid for pytest, handle cases when directory name includes dot/hyphen/space
Args:
path: absolute path
file_abs_path: absolute file path
Returns: relative path based on os.getcwd()
Returns:
ensured valid absolute file path
"""
if os.path.isabs(path):
return path[len(os.getcwd()) + 1 :]
else:
return path
project_meta = load_project_meta(file_abs_path)
raw_abs_file_name, file_suffix = os.path.splitext(file_abs_path)
file_suffix = file_suffix.lower()
raw_file_relative_name = convert_relative_project_root_dir(raw_abs_file_name)
if raw_file_relative_name == "":
return file_abs_path
path_names = []
for name in raw_file_relative_name.rstrip(os.sep).split(os.sep):
if name[0] in string.digits:
# ensure file name not startswith digit
# 19 => T19, 2C => T2C
name = f"T{name}"
if name.startswith("."):
# avoid ".csv" been converted to "_csv"
pass
else:
# handle cases when directory name includes dot/hyphen/space
name = name.replace(" ", "_").replace(".", "_").replace("-", "_")
path_names.append(name)
new_file_path = os.path.join(
project_meta.RootDir, f"{os.sep.join(path_names)}{file_suffix}"
)
return new_file_path
def __ensure_testcase_module(path: Text) -> NoReturn:
@@ -111,43 +143,18 @@ def __ensure_testcase_module(path: Text) -> NoReturn:
f.write("# NOTICE: Generated By HttpRunner. DO NOT EDIT!\n")
def __ensure_project_meta_files(tests_path: Text) -> NoReturn:
""" ensure project meta files exist in generated pytest folder files
include debugtalk.py and .env
"""
project_meta = load_project_meta(tests_path)
# handle cases when generated pytest directory are different from original yaml/json testcases
debugtalk_path = project_meta.debugtalk_path
if debugtalk_path:
debugtalk_new_path = ensure_file_path_valid(debugtalk_path)
if debugtalk_new_path != debugtalk_path:
logger.info(f"copy debugtalk.py to {debugtalk_new_path}")
copyfile(debugtalk_path, debugtalk_new_path)
global pytest_files_made_cache_mapping
pytest_files_made_cache_mapping[debugtalk_new_path] = ""
dot_csv_path = project_meta.dot_env_path
if dot_csv_path:
dot_csv_new_path = ensure_file_path_valid(dot_csv_path)
if dot_csv_new_path != dot_csv_path:
logger.info(f"copy .env to {dot_csv_new_path}")
copyfile(dot_csv_path, dot_csv_new_path)
def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]:
def convert_testcase_path(testcase_abs_path: Text) -> Tuple[Text, Text]:
"""convert single YAML/JSON testcase path to python file"""
testcase_new_path = ensure_file_path_valid(testcase_path)
testcase_new_path = ensure_file_abs_path_valid(testcase_abs_path)
dir_path = os.path.dirname(testcase_new_path)
file_name, _ = os.path.splitext(os.path.basename(testcase_new_path))
testcase_python_path = os.path.join(dir_path, f"{file_name}_test.py")
testcase_python_abs_path = os.path.join(dir_path, f"{file_name}_test.py")
# convert title case, e.g. request_with_variables => RequestWithVariables
name_in_title_case = file_name.title().replace("_", "")
return testcase_python_path, name_in_title_case
return testcase_python_abs_path, name_in_title_case
def format_pytest_with_black(*python_paths: Text) -> NoReturn:
@@ -184,6 +191,9 @@ def make_config_chain_style(config: Dict) -> Text:
if "export" in config:
config_chain_style += f'.export(*{config["export"]})'
if "weight" in config:
config_chain_style += f'.locust_weight({config["weight"]})'
return config_chain_style
@@ -316,18 +326,20 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
testcase_abs_path = __ensure_absolute(testcase["config"]["path"])
logger.info(f"start to make testcase: {testcase_abs_path}")
testcase_python_path, testcase_cls_name = convert_testcase_path(testcase_abs_path)
testcase_python_abs_path, testcase_cls_name = convert_testcase_path(
testcase_abs_path
)
if dir_path:
testcase_python_path = os.path.join(
dir_path, os.path.basename(testcase_python_path)
testcase_python_abs_path = os.path.join(
dir_path, os.path.basename(testcase_python_abs_path)
)
global pytest_files_made_cache_mapping
if testcase_python_path in pytest_files_made_cache_mapping:
return testcase_python_path
if testcase_python_abs_path in pytest_files_made_cache_mapping:
return testcase_python_abs_path
config = testcase["config"]
config["path"] = __ensure_cwd_relative(testcase_python_path)
config["path"] = convert_relative_project_root_dir(testcase_python_abs_path)
config["variables"] = convert_variables(
config.get("variables", {}), testcase_abs_path
)
@@ -348,25 +360,32 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
test_content = ensure_testcase_v3_api(test_content)
test_content.setdefault("config", {})["path"] = ref_testcase_path
ref_testcase_python_path = make_testcase(test_content)
ref_testcase_python_abs_path = make_testcase(test_content)
# prepare ref testcase class name
ref_testcase_cls_name = pytest_files_made_cache_mapping[
ref_testcase_python_path
ref_testcase_python_abs_path
]
teststep["testcase"] = ref_testcase_cls_name
# prepare import ref testcase
ref_testcase_python_path = ref_testcase_python_path[len(os.getcwd()) + 1 :]
ref_module_name, _ = os.path.splitext(ref_testcase_python_path)
ref_testcase_python_relative_path = convert_relative_project_root_dir(
ref_testcase_python_abs_path
)
ref_module_name, _ = os.path.splitext(ref_testcase_python_relative_path)
ref_module_name = ref_module_name.replace(os.sep, ".")
imports_list.append(
f"from {ref_module_name} import TestCase{ref_testcase_cls_name} as {ref_testcase_cls_name}"
)
testcase_path = convert_relative_project_root_dir(testcase_abs_path)
# current file compared to ProjectRootDir
diff_levels = len(testcase_path.split(os.sep))
data = {
"version": __version__,
"testcase_path": __ensure_cwd_relative(testcase_abs_path),
"testcase_path": testcase_path,
"diff_levels": diff_levels,
"class_name": f"TestCase{testcase_cls_name}",
"imports_list": imports_list,
"config_chain_style": make_config_chain_style(config),
@@ -377,19 +396,19 @@ def make_testcase(testcase: Dict, dir_path: Text = None) -> Text:
content = __TEMPLATE__.render(data)
# ensure new file's directory exists
dir_path = os.path.dirname(testcase_python_path)
dir_path = os.path.dirname(testcase_python_abs_path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)
with open(testcase_python_path, "w", encoding="utf-8") as f:
with open(testcase_python_abs_path, "w", encoding="utf-8") as f:
f.write(content)
pytest_files_made_cache_mapping[testcase_python_path] = testcase_cls_name
__ensure_testcase_module(testcase_python_path)
pytest_files_made_cache_mapping[testcase_python_abs_path] = testcase_cls_name
__ensure_testcase_module(testcase_python_abs_path)
logger.info(f"generated testcase: {testcase_python_path}")
logger.info(f"generated testcase: {testcase_python_abs_path}")
return testcase_python_path
return testcase_python_abs_path
def make_testsuite(testsuite: Dict) -> NoReturn:
@@ -406,7 +425,7 @@ def make_testsuite(testsuite: Dict) -> NoReturn:
logger.info(f"start to make testsuite: {testsuite_path}")
# create directory with testsuite file name, put its testcases under this directory
testsuite_path = ensure_file_path_valid(testsuite_path)
testsuite_path = ensure_file_abs_path_valid(testsuite_path)
testsuite_dir, file_suffix = os.path.splitext(testsuite_path)
# demo_testsuite.yml => demo_testsuite_yml
testsuite_dir = f"{testsuite_dir}_{file_suffix.lstrip('.')}"
@@ -442,6 +461,10 @@ def make_testsuite(testsuite: Dict) -> NoReturn:
)
testcase_dict["config"]["variables"].update(testcase_variables)
# override weight
weight = testcase.get("weight", 1)
testcase_dict["config"]["weight"] = weight
# make testcase
testcase_pytest_path = make_testcase(testcase_dict, testsuite_dir)
pytest_files_run_set.add(testcase_pytest_path)
@@ -455,6 +478,7 @@ def __make(tests_path: Text) -> NoReturn:
tests_path: should be in absolute path
"""
logger.info(f"make path: {tests_path}")
test_files = []
if os.path.isdir(tests_path):
files_list = load_folder_files(tests_path)
@@ -472,11 +496,14 @@ def __make(tests_path: Text) -> NoReturn:
try:
test_content = load_test_file(test_file)
except (exceptions.FileNotFound, exceptions.FileFormatError) as ex:
logger.warning(ex)
logger.warning(f"Invalid test file: {test_file}\n{type(ex).__name__}: {ex}")
continue
if not isinstance(test_content, Dict):
logger.warning(f"test content not in dict format. \npath: {test_file}")
logger.warning(
f"Invalid test file: {test_file}\n"
f"reason: test content not in dict format."
)
continue
# api in v2 format, convert to v3 testcase
@@ -485,10 +512,18 @@ def __make(tests_path: Text) -> NoReturn:
if "config" not in test_content:
logger.warning(
f"Invalid testcase/testsuite: missing config part in testcase/testsuite.\npath: {test_file}"
f"Invalid testcase/testsuite file: {test_file}\n"
f"reason: missing config part."
)
continue
elif not isinstance(test_content["config"], Dict):
logger.warning(
f"Invalid testcase/testsuite file: {test_file}\n"
f"reason: config should be dict type, got {test_content['config']}"
)
continue
# ensure path absolute
test_content.setdefault("config", {})["path"] = test_file
# testcase
@@ -496,19 +531,28 @@ def __make(tests_path: Text) -> NoReturn:
try:
testcase_pytest_path = make_testcase(test_content)
pytest_files_run_set.add(testcase_pytest_path)
except exceptions.TestCaseFormatError:
except exceptions.TestCaseFormatError as ex:
logger.warning(
f"Invalid testcase file: {test_file}\n{type(ex).__name__}: {ex}"
)
continue
# testsuite
elif "testcases" in test_content:
try:
make_testsuite(test_content)
except exceptions.TestSuiteFormatError:
except exceptions.TestSuiteFormatError as ex:
logger.warning(
f"Invalid testsuite file: {test_file}\n{type(ex).__name__}: {ex}"
)
continue
# invalid format
else:
logger.warning(f"skip invalid testcase/testsuite file: {test_file}")
logger.warning(
f"Invalid test file: {test_file}\n"
f"reason: file content is neither testcase nor testsuite"
)
def main_make(tests_paths: List[Text]) -> List[Text]:
@@ -526,8 +570,6 @@ def main_make(tests_paths: List[Text]) -> List[Text]:
logger.error(ex)
sys.exit(1)
__ensure_project_meta_files(tests_path)
# format pytest files
pytest_files_format_list = pytest_files_made_cache_mapping.keys()
format_pytest_with_black(*pytest_files_format_list)

View File

@@ -41,6 +41,7 @@ class TConfig(BaseModel):
# teardown_hooks: Hooks = []
export: Export = []
path: Text = None
weight: int = 1
class TRequest(BaseModel):
@@ -85,7 +86,7 @@ class ProjectMeta(BaseModel):
dot_env_path: Text = "" # .env file path
functions: FunctionsMapping = {} # functions defined in debugtalk.py
env: Env = {}
RootDir: Text = os.getcwd() # project root directory, the path debugtalk.py located
RootDir: Text = os.getcwd() # project root directory (ensure absolute), the path debugtalk.py located
class TestsMapping(BaseModel):
@@ -122,7 +123,7 @@ class RequestData(BaseModel):
url: Url
headers: Headers = {}
cookies: Cookies = {}
body: Union[Text, bytes, Dict, None] = {}
body: Union[Text, bytes, Dict, List, None] = {}
class ResponseData(BaseModel):

View File

@@ -3,6 +3,7 @@ import builtins
import re
from typing import Any, Set, Text, Callable, List, Dict
from loguru import logger
from sentry_sdk import capture_exception
from httprunner import loader, utils, exceptions
@@ -331,10 +332,20 @@ def parse_string(
function_meta = parse_function_params(func_params_str)
args = function_meta["args"]
kwargs = function_meta["kwargs"]
parsed_args = parse_data(args, variables_mapping, functions_mapping)
parsed_kwargs = parse_data(kwargs, variables_mapping, functions_mapping)
func_eval_value = func(*parsed_args, **parsed_kwargs)
try:
func_eval_value = func(*parsed_args, **parsed_kwargs)
except Exception as ex:
logger.error(
f"call function error:\n"
f"func_name: {func_name}\n"
f"args: {parsed_args}\n"
f"kwargs: {parsed_kwargs}\n"
f"{type(ex).__name__}: {ex}"
)
raise
func_raw_str = "${" + func_name + f"({func_params_str})" + "}"
if func_raw_str == raw_string:

View File

@@ -203,24 +203,30 @@ class HttpRunner(object):
# validate
validators = step.validators
session_success = False
try:
resp_obj.validate(
validators, variables_mapping, self.__project_meta.functions
)
self.__session.data.success = True
session_success = True
except ValidationFailure:
self.__session.data.success = False
session_success = False
log_req_resp_details()
# log testcase duration before raise ValidationFailure
self.__duration = time.time() - self.__start_at
raise
finally:
# save request & response meta data
self.__session.data.validators = resp_obj.validation_results
self.success = self.__session.data.success
# save step data
step_data.success = self.__session.data.success
step_data.data = self.__session.data
self.success = session_success
step_data.success = session_success
if hasattr(self.__session, "data"):
# httprunner.client.HttpSession, not locust.clients.HttpSession
# save request & response meta data
self.__session.data.success = session_success
self.__session.data.validators = resp_obj.validation_results
# save step data
step_data.data = self.__session.data
return step_data

View File

@@ -17,18 +17,23 @@ class Config(object):
self.__base_url = ""
self.__verify = False
self.__export = []
self.__weight = 1
caller_frame = inspect.stack()[1]
self.__path = caller_frame.filename
@property
def name(self):
def name(self) -> Text:
return self.__name
@property
def path(self):
def path(self) -> Text:
return self.__path
@property
def weight(self) -> int:
return self.__weight
def variables(self, **variables) -> "Config":
self.__variables.update(variables)
return self
@@ -45,6 +50,10 @@ class Config(object):
self.__export.extend(export_var_name)
return self
def locust_weight(self, weight: int) -> "Config":
self.__weight = weight
return self
def perform(self) -> TConfig:
return TConfig(
name=self.__name,
@@ -53,6 +62,7 @@ class Config(object):
variables=self.__variables,
export=list(set(self.__export)),
path=self.__path,
weight=self.__weight,
)

View File

@@ -3,16 +3,14 @@ import copy
import json
import os.path
import platform
import string
import uuid
from typing import Dict, List, Any, Text
from typing import Dict, List, Any
import sentry_sdk
from loguru import logger
from httprunner import __version__
from httprunner import exceptions
from httprunner.models import VariablesMapping
from loguru import logger
def init_sentry_sdk():
@@ -181,48 +179,6 @@ def sort_dict_by_custom_order(raw_dict: Dict, custom_order: List):
)
def ensure_file_path_valid(file_path: Text) -> Text:
""" ensure file path valid for pytest, handle cases when directory name includes dot/hyphen/space
Args:
file_path: absolute or relative file path
Returns:
ensured valid absolute file path
"""
raw_file_name, file_suffix = os.path.splitext(file_path)
file_suffix = file_suffix.lower()
if os.path.isabs(file_path):
raw_file_relative_name = raw_file_name[len(os.getcwd()) + 1 :]
else:
raw_file_relative_name = raw_file_name
if raw_file_relative_name == "":
return file_path
path_names = []
for name in raw_file_relative_name.rstrip(os.sep).split(os.sep):
if name[0] in string.digits:
# ensure file name not startswith digit
# 19 => T19, 2C => T2C
name = f"T{name}"
if name.startswith("."):
# avoid ".csv" been converted to "_csv"
pass
else:
# handle cases when directory name includes dot/hyphen/space
name = name.replace(" ", "_").replace(".", "_").replace("-", "_")
path_names.append(name)
new_file_path = os.path.join(os.getcwd(), f"{os.sep.join(path_names)}{file_suffix}")
return new_file_path
class ExtendJSONEncoder(json.JSONEncoder):
""" especially used to safely dump json data with python object, such as MultipartEncoder
"""

View File

@@ -59,5 +59,6 @@ nav:
- Write Testcase: user/write_testcase.md
- Run Testcase: user/run_testcase.md
- Testing Report: user/testing_report.md
- Run load test: user/run_loadtest.md
- Sponsors: sponsors.md
- CHANGELOG: CHANGELOG.md

558
poetry.lock generated
View File

@@ -95,7 +95,19 @@ description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
optional = false
python-versions = "*"
version = "2020.4.5.1"
version = "2020.4.5.2"
[[package]]
category = "main"
description = "Foreign Function Interface for Python calling C code."
marker = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""
name = "cffi"
optional = true
python-versions = "*"
version = "1.14.0"
[package.dependencies]
pycparser = "*"
[[package]]
category = "main"
@@ -122,6 +134,17 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.3"
[[package]]
category = "main"
description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables."
name = "configargparse"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.2.3"
[package.extras]
yaml = ["pyyaml"]
[[package]]
category = "main"
description = "PEP 567 Backport"
@@ -177,6 +200,80 @@ optional = true
python-versions = "*"
version = "1.0.7"
[[package]]
category = "main"
description = "A simple framework for building complex web applications."
name = "flask"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.1.2"
[package.dependencies]
Jinja2 = ">=2.10.1"
Werkzeug = ">=0.15"
click = ">=5.1"
itsdangerous = ">=0.24"
[package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
dotenv = ["python-dotenv"]
[[package]]
category = "main"
description = "HTTP basic access authentication for Flask."
name = "flask-basicauth"
optional = true
python-versions = "*"
version = "0.2.0"
[package.dependencies]
Flask = "*"
[[package]]
category = "main"
description = "Coroutine-based network library"
name = "gevent"
optional = true
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
version = "20.6.2"
[package.dependencies]
cffi = ">=1.12.2"
greenlet = ">=0.4.16"
setuptools = "*"
"zope.event" = "*"
"zope.interface" = "*"
[package.extras]
dnspython = ["dnspython (>=1.16.0)", "idna"]
docs = ["repoze.sphinx.autointerface", "sphinxcontrib-programoutput"]
monitor = ["psutil (>=5.7.0)"]
recommended = ["dnspython (>=1.16.0)", "idna", "cffi (>=1.12.2)", "selectors2", "backports.socketpair", "psutil (>=5.7.0)"]
test = ["dnspython (>=1.16.0)", "idna", "requests", "objgraph", "cffi (>=1.12.2)", "selectors2", "futures", "mock", "backports.socketpair", "contextvars (2.4)", "coverage (<5.0)", "coveralls (>=1.7.0)", "psutil (>=5.7.0)"]
[[package]]
category = "main"
description = "http client library for gevent"
name = "geventhttpclient"
optional = true
python-versions = "*"
version = "1.4.2"
[package.dependencies]
certifi = "*"
gevent = ">=0.13"
six = "*"
[[package]]
category = "main"
description = "Lightweight in-process concurrent programming"
marker = "platform_python_implementation == \"CPython\""
name = "greenlet"
optional = true
python-versions = "*"
version = "0.4.16"
[[package]]
category = "dev"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
@@ -221,14 +318,22 @@ marker = "python_version < \"3.8\""
name = "importlib-metadata"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
version = "1.6.0"
version = "1.6.1"
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "rst.linker"]
testing = ["packaging", "importlib-resources"]
testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
[[package]]
category = "main"
description = "Various helpers to pass data to untrusted environments and back."
name = "itsdangerous"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.1.0"
[[package]]
category = "main"
@@ -252,6 +357,25 @@ optional = false
python-versions = "*"
version = "0.9.5"
[[package]]
category = "main"
description = "Developer friendly load testing framework"
name = "locust"
optional = true
python-versions = ">=3.6"
version = "1.0.3"
[package.dependencies]
ConfigArgParse = ">=1.0"
Flask-BasicAuth = ">=0.2.0"
flask = ">=1.1.2"
gevent = ">=1.5.0"
geventhttpclient = ">=1.4.2"
msgpack = ">=0.6.2"
psutil = ">=5.6.7"
pyzmq = ">=16.0.2"
requests = ">=2.9.1"
[[package]]
category = "main"
description = "Python logging made (stupidly) simple"
@@ -285,7 +409,15 @@ description = "More routines for operating on iterables, beyond itertools"
name = "more-itertools"
optional = false
python-versions = ">=3.5"
version = "8.3.0"
version = "8.4.0"
[[package]]
category = "main"
description = "MessagePack (de)serializer."
name = "msgpack"
optional = true
python-versions = "*"
version = "1.0.0"
[[package]]
category = "main"
@@ -323,13 +455,33 @@ version = ">=0.12"
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
category = "main"
description = "Cross-platform lib for process and system monitoring in Python."
name = "psutil"
optional = true
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "5.7.0"
[package.extras]
enum = ["enum34"]
[[package]]
category = "main"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.8.1"
version = "1.8.2"
[[package]]
category = "main"
description = "C parser in Python"
marker = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""
name = "pycparser"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.20"
[[package]]
category = "main"
@@ -363,7 +515,7 @@ description = "pytest: simple powerful testing with Python"
name = "pytest"
optional = false
python-versions = ">=3.5"
version = "5.4.2"
version = "5.4.3"
[package.dependencies]
atomicwrites = ">=1.0"
@@ -414,13 +566,21 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "5.3.1"
[[package]]
category = "main"
description = "Python bindings for 0MQ"
name = "pyzmq"
optional = true
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"
version = "19.0.1"
[[package]]
category = "main"
description = "Alternative regular expression module, to replace re."
name = "regex"
optional = false
python-versions = "*"
version = "2020.5.14"
version = "2020.6.8"
[[package]]
category = "main"
@@ -428,7 +588,7 @@ description = "Python HTTP for Humans."
name = "requests"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.23.0"
version = "2.24.0"
[package.dependencies]
certifi = ">=2017.4.17"
@@ -554,11 +714,11 @@ version = "0.14.0"
[[package]]
category = "main"
description = "Measures number of Terminal column cells of wide-character codes"
description = "Measures the displayed width of unicode strings in a terminal"
name = "wcwidth"
optional = false
python-versions = "*"
version = "0.1.9"
version = "0.2.4"
[[package]]
category = "dev"
@@ -568,6 +728,18 @@ optional = false
python-versions = ">=3.6"
version = "8.0.2"
[[package]]
category = "main"
description = "The comprehensive WSGI web application library."
name = "werkzeug"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.0.1"
[package.extras]
dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
watchdog = ["watchdog"]
[[package]]
category = "main"
description = "A small Python utility to set file creation time on Windows"
@@ -593,12 +765,44 @@ version = "3.1.0"
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[[package]]
category = "main"
description = "Very basic event publishing system"
name = "zope.event"
optional = true
python-versions = "*"
version = "4.4"
[package.dependencies]
setuptools = "*"
[package.extras]
docs = ["sphinx"]
test = ["zope.testrunner"]
[[package]]
category = "main"
description = "Interfaces for Python"
name = "zope.interface"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "5.1.0"
[package.dependencies]
setuptools = "*"
[package.extras]
docs = ["sphinx", "repoze.sphinx.autointerface"]
test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[extras]
allure = ["allure-pytest"]
locust = ["locust"]
upload = ["requests-toolbelt", "filetype"]
[metadata]
content-hash = "581cacf33c8afe330e5b6a965d5e16f6266718249cbdfed0d080b7536c5c4590"
content-hash = "e47b74393605334573a83551e91effda69586b32c65e1acb468901bf5596374d"
python-versions = "^3.6"
[metadata.files]
@@ -631,8 +835,38 @@ black = [
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
]
certifi = [
{file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"},
{file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"},
{file = "certifi-2020.4.5.2-py2.py3-none-any.whl", hash = "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"},
{file = "certifi-2020.4.5.2.tar.gz", hash = "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1"},
]
cffi = [
{file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"},
{file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"},
{file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"},
{file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"},
{file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"},
{file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"},
{file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"},
{file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"},
{file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"},
{file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"},
{file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"},
{file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"},
{file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"},
{file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"},
{file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"},
{file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"},
{file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"},
{file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"},
{file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"},
{file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"},
{file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"},
{file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"},
{file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"},
{file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"},
{file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"},
{file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"},
{file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"},
{file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"},
]
chardet = [
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
@@ -646,6 +880,9 @@ colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
]
configargparse = [
{file = "ConfigArgParse-1.2.3.tar.gz", hash = "sha256:edd17be986d5c1ba2e307150b8e5f5107aba125f3574dddd02c85d5cdcfd37dc"},
]
contextvars = [
{file = "contextvars-2.4.tar.gz", hash = "sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"},
]
@@ -695,6 +932,110 @@ filetype = [
{file = "filetype-1.0.7-py2.py3-none-any.whl", hash = "sha256:353369948bb1c09b8b3ea3d78390b5586e9399bff9aab894a1dff954e31a66f6"},
{file = "filetype-1.0.7.tar.gz", hash = "sha256:da393ece8d98b47edf2dd5a85a2c8733e44b769e32c71af4cd96ed8d38d96aa7"},
]
flask = [
{file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
{file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
]
flask-basicauth = [
{file = "Flask-BasicAuth-0.2.0.tar.gz", hash = "sha256:df5ebd489dc0914c224419da059d991eb72988a01cdd4b956d52932ce7d501ff"},
]
gevent = [
{file = "gevent-20.6.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:b03890bbddbae5667f5baad517417056496ff5e92c3c7945b27cc08f55a9fcb2"},
{file = "gevent-20.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1ea0d34cb78cdf37870be3bfb9330ebda89197bed9e048c14f4a90dec19a33e0"},
{file = "gevent-20.6.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:73eb4cf3114fbb5dd801bd0b93941adfa2fa6d99e91976c20a121ea14b8b39b9"},
{file = "gevent-20.6.2-cp27-cp27m-win32.whl", hash = "sha256:f41cc8e853ac2252bc58f6feabd74b8aae613e2d19097c5373463122f4dc08f0"},
{file = "gevent-20.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:d3baff87d935a5eeffb0e4f7cd5ffe258d2430cd62aeee2e5396f85da07df435"},
{file = "gevent-20.6.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:7d8408854ce892f987305a0e9bf5c051f4ea29453665454396d6afb620c719b6"},
{file = "gevent-20.6.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:ea2e4584950186b71d648bde6af40dae4d4c6f43db25a732ec056b27a7a83afe"},
{file = "gevent-20.6.2-cp35-cp35m-win32.whl", hash = "sha256:c0f4340e40e0f9dfe93a52a12ddf5b1eeda9bbc89b99bf3b9b23acab0dfae0a4"},
{file = "gevent-20.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:13c74d6784ef5ada2666abf2bb310d27a1d14291f7cac46148f336b19f714d40"},
{file = "gevent-20.6.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:78bd94f6f2ac366155169df3507068f6381f2ad77625633189ce183f86a57597"},
{file = "gevent-20.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0b16dd85eddaf6acdad373ce90ed4da09ef466cbc5e0ee5932d13f099929e844"},
{file = "gevent-20.6.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a47556cac07e31b3cef8fd701599b3b1365961fe3736471f41807ffa27c5c848"},
{file = "gevent-20.6.2-cp36-cp36m-win32.whl", hash = "sha256:bef18b8bd3b728240b9bbd699737216b793d6c97b482431f69dcbe328ad73692"},
{file = "gevent-20.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d0a67a20ce325f6a2068e0bd9fbf83db8a5f5ced972ed8ac5c20079a7d98c7d1"},
{file = "gevent-20.6.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:b17915b65b49a425115ddc3087484c81b1e47ce38c931d18bb14e453753e4d06"},
{file = "gevent-20.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ebb8a545112110e3a6edf905ae1556b0538fc148c743aa7d8cfaebbbc23de31d"},
{file = "gevent-20.6.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6c864b5604166ac8351e3128a1135b883b9e978fd24afbd75a249dcb42bc8ab5"},
{file = "gevent-20.6.2-cp37-cp37m-win32.whl", hash = "sha256:e5ca5ee80a9d9e697c9fc22b4bbce9ad06870f83fc8e7774e5504892ef702476"},
{file = "gevent-20.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f2a02d9004ccb18edd9eaf6f25da9a7763de41a69754d5e4d872a8cbf8bd0b72"},
{file = "gevent-20.6.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:354f932c284fa45826b32f42927d892096cce05671b50b3ff59528230217ad47"},
{file = "gevent-20.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:67776cb33b638a3c61a0351d9d1e8f33a46b47de619e249de1159892f9ff035c"},
{file = "gevent-20.6.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:68764aca061bbbbade43727e797f9c28042f6d90cca5fb6514ef726d43ab00ca"},
{file = "gevent-20.6.2-cp38-cp38-win32.whl", hash = "sha256:0f3fbb1703b10609856e5dffb0e358bf5edf57e52dc7cd7226e3f8674fdc0a0f"},
{file = "gevent-20.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:a18d8dd9bfa994a22f30adfa0563d80f0809140045c34f85535f422813d25855"},
{file = "gevent-20.6.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:9527087984f1659be899b3300d5d61c7c5b01d8beae106aff5160316da8bc56f"},
{file = "gevent-20.6.2-pp27-pypy_73-macosx_10_7_x86_64.whl", hash = "sha256:76ef4c6e3332e6f7278142d791b28695adfce39735900fccef2a0f1d894f6b36"},
{file = "gevent-20.6.2-pp27-pypy_73-win32.whl", hash = "sha256:3cb2f6978615d52e4e4e667b035c11a7272bb68b14d119faf1b138164b2f354f"},
{file = "gevent-20.6.2.tar.gz", hash = "sha256:a23c2abf08e851c988723f6a2996d495f513a2c0dc70f9956af03af8debdb5d1"},
]
geventhttpclient = [
{file = "geventhttpclient-1.4.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:7b7e827418ecc926271111ef9a5f832063c28250487d77292442a94dce040ab8"},
{file = "geventhttpclient-1.4.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:6f662227cf8ec2f061d67e9bcc4e2a83ccae4b91c812a0f4a8dd4b239300860f"},
{file = "geventhttpclient-1.4.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5c7a6211bd5747edf7e38a489369c510d06cfd293ec56be1bdd2045766075f8b"},
{file = "geventhttpclient-1.4.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:7e0686068f4dca0148e98957ac6804acb819d4a32886b75e4af8b7a4fafb0729"},
{file = "geventhttpclient-1.4.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8d2874c86012b9c34de7a610b3e7762eca80da7591f945464afe3b024f99ea38"},
{file = "geventhttpclient-1.4.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e089d1613fc30fd631cd82223abd8187bd9f3ccba70a58df38eb8139928c9237"},
{file = "geventhttpclient-1.4.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f2c2d6a8c5973d1ab918443a7739ddc4bb1b3948ca676feccfe6b10076a13275"},
{file = "geventhttpclient-1.4.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:565dff607131b04b8a2e302065f47be235a2adefe30443401f2dc3d58972d18f"},
{file = "geventhttpclient-1.4.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:bcad86491ef10b281f43a93ca65bb65aec6afc7856badc8cb4745daa4b43fe33"},
{file = "geventhttpclient-1.4.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:e4179375224242a64e965d709282af91491a6de40f2117df81a2a775f94d2bc3"},
{file = "geventhttpclient-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:49e0aed4b7cc7eef09294376c77690528e4a21a37a8a0e99efa940561183a0f5"},
{file = "geventhttpclient-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9324187592cbd66764a28a8f49dafb2bf68a07612bb4adf27a0777a35ba53cb6"},
{file = "geventhttpclient-1.4.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:b6cbcc500635b228a39c2a0b290059bdc8f7254776b6bc628bd7384ce0446125"},
{file = "geventhttpclient-1.4.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:682581949303c175abbaf686df819d403104ecc6b0355108c6cc473cc539a505"},
{file = "geventhttpclient-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:3df9cad44fddc775b06f7a9e8bea763bb8198ba426a8ce62bb79cb44bb7e419f"},
{file = "geventhttpclient-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:052899e60f63529a5ffc581987fef2893f445a62ebdb5f51f02444b9153b9e00"},
{file = "geventhttpclient-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:edd11d6b22993008a98235f02344f79783d7727f05d4cd4cc638591b2cf00346"},
{file = "geventhttpclient-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a391afadf1d7005d1f584470c5a2b03588c23c25b42ebed6a385ffb1e5deb61b"},
{file = "geventhttpclient-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:80570bf577de010cb3cff704bc9df2d76b3398144e6224e35558a9f3f794f863"},
{file = "geventhttpclient-1.4.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:df9ce998dbaef4e4aa6d9b2bc129d69c9ec41a71df30808298f6b8cc2e3cee18"},
{file = "geventhttpclient-1.4.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3becac3ec54cc74caadcf87a309343d3298a8ca0ef6d3c4f7ab197be0acefdcb"},
{file = "geventhttpclient-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:a2ba13a6dc4343e9be64288fb89c077586350d201945565b5b882431a7cc111e"},
{file = "geventhttpclient-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:79a302b9448fee4d2a334278c0cb679195ec0ed4de0832bf46797e90602d5832"},
{file = "geventhttpclient-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bef49de16b0c4551731294e37a05b55415fc49fa3c4e5b6cfac0d41fd430627d"},
{file = "geventhttpclient-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a6eb1aa56bc52ba0adb281f7977662db8661b1398866674cf9be559cf23e2765"},
{file = "geventhttpclient-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dade5b1266e10f78104a4880163b1aca1ace8c1efbf0e71887b9cff78bfbdb2f"},
{file = "geventhttpclient-1.4.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d09d59902ca62253a48f7a5ffbd6c591cca8a65fe15428a294f600333b891aaa"},
{file = "geventhttpclient-1.4.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:b9bf31aed10e40116357f6eb1f3ac997030485ea8afd75fd00644dc6ccba1614"},
{file = "geventhttpclient-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:9141b2ce49adb317cf036a70f4543319abfde625b92e780794d806bcc1bcd1de"},
{file = "geventhttpclient-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:302277ba05f941da34fac3fdd9e0f13693c8460fa82fd8d40aa34151537fc171"},
{file = "geventhttpclient-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d8f2bbbc0f1cfd8d0b5b1c839be513e32a4d94c79f3b28aed5e0443866ee1ce5"},
{file = "geventhttpclient-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6018fc296c4d6a1ca0cfdf3a9fb23792e2302cbf5717dd0a25ee0c084bf8216a"},
{file = "geventhttpclient-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b68fbaff99f8a7701477b83e829b74c0f6d878953dea468cc52601093a4806d4"},
{file = "geventhttpclient-1.4.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:94bf3480828190cb0cf3fae350b760aa12d9912b8eed5449ddb25867edae31f6"},
{file = "geventhttpclient-1.4.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28e4ef874d4153460c1e29323f543942a6b3be308c5a50c006d412f4f9e7fc9c"},
{file = "geventhttpclient-1.4.2-cp38-cp38-win32.whl", hash = "sha256:1dcc1995146edbcb98e6343137ee4444fa5966ae534e5fac647b32357479e93f"},
{file = "geventhttpclient-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:355417460ce971a01f1ee0b4c53c65505faa0e1cc757ed16d8cdcbc5e66b21ab"},
{file = "geventhttpclient-1.4.2-pp27-pypy_73-macosx_10_7_x86_64.whl", hash = "sha256:074f9914ce8320f71ffaa1d48247a5f2d551e0df4a225e50d8cf162128116a6b"},
{file = "geventhttpclient-1.4.2-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:71bc17ffab758b800d1ca1c0ac0c1a47aa8dd7e80c88e9c6b454f7082ab03f8a"},
{file = "geventhttpclient-1.4.2-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:a5a9e5dc95618bf1d061de6862429f90bb8740b59449a47bfdadd4cc94eebb9f"},
{file = "geventhttpclient-1.4.2-pp27-pypy_73-win32.whl", hash = "sha256:3b18bb76b175d36205889e2c33c1746b70ab156392d6d5f382d1b2b349fd53e1"},
{file = "geventhttpclient-1.4.2-pp36-pypy36_pp73-macosx_10_7_x86_64.whl", hash = "sha256:dfa24eaf4310fc99c796fafac527e7c839670f15144936ab885b43705f2d6086"},
{file = "geventhttpclient-1.4.2-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:86feb2e8f2210af353fb00e85db672007e43dd993f53dab18453b0b230fd0673"},
{file = "geventhttpclient-1.4.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:d393b8a84dd9edcfb92ebfd35065a19eb589ce082dae4b61222c5835146c81c7"},
{file = "geventhttpclient-1.4.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:0271d8ea2d4ad5b3fb753fb1df7e00aadfbe7d50d26fed714b97755f04011864"},
{file = "geventhttpclient-1.4.2.tar.gz", hash = "sha256:967b11c4a37032f98c08f58176e4ac8de10473ab0c1f617acb8202d44b97fe21"},
]
greenlet = [
{file = "greenlet-0.4.16-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:80cb0380838bf4e48da6adedb0c7cd060c187bb4a75f67a5aa9ec33689b84872"},
{file = "greenlet-0.4.16-cp27-cp27m-win32.whl", hash = "sha256:df7de669cbf21de4b04a3ffc9920bc8426cab4c61365fa84d79bf97401a8bef7"},
{file = "greenlet-0.4.16-cp27-cp27m-win_amd64.whl", hash = "sha256:1429dc183b36ec972055e13250d96e174491559433eb3061691b446899b87384"},
{file = "greenlet-0.4.16-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5ea034d040e6ab1d2ae04ab05a3f37dbd719c4dee3804b13903d4cc794b1336e"},
{file = "greenlet-0.4.16-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c196a5394c56352e21cb7224739c6dd0075b69dd56f758505951d1d8d68cf8a9"},
{file = "greenlet-0.4.16-cp35-cp35m-win32.whl", hash = "sha256:1000038ba0ea9032948e2156a9c15f5686f36945e8f9906e6b8db49f358e7b52"},
{file = "greenlet-0.4.16-cp35-cp35m-win_amd64.whl", hash = "sha256:1b805231bfb7b2900a16638c3c8b45c694334c811f84463e52451e00c9412691"},
{file = "greenlet-0.4.16-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e5db19d4a7d41bbeb3dd89b49fc1bc7e6e515b51bbf32589c618655a0ebe0bf0"},
{file = "greenlet-0.4.16-cp36-cp36m-win32.whl", hash = "sha256:eac2a3f659d5f41d6bbfb6a97733bc7800ea5e906dc873732e00cebb98cec9e4"},
{file = "greenlet-0.4.16-cp36-cp36m-win_amd64.whl", hash = "sha256:7eed31f4efc8356e200568ba05ad645525f1fbd8674f1e5be61a493e715e3873"},
{file = "greenlet-0.4.16-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:682328aa576ec393c1872615bcb877cf32d800d4a2f150e1a5dc7e56644010b1"},
{file = "greenlet-0.4.16-cp37-cp37m-win32.whl", hash = "sha256:3a35e33902b2e6079949feed7a2dafa5ac6f019da97bd255842bb22de3c11bf5"},
{file = "greenlet-0.4.16-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b2a984bbfc543d144d88caad6cc7ff4a71be77102014bd617bd88cfb038727"},
{file = "greenlet-0.4.16-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:d83c1d38658b0f81c282b41238092ed89d8f93c6e342224ab73fb39e16848721"},
{file = "greenlet-0.4.16-cp38-cp38-win32.whl", hash = "sha256:e695ac8c3efe124d998230b219eb51afb6ef10524a50b3c45109c4b77a8a3a92"},
{file = "greenlet-0.4.16-cp38-cp38-win_amd64.whl", hash = "sha256:133ba06bad4e5f2f8bf6a0ac434e0fd686df749a86b3478903b92ec3a9c0c90b"},
{file = "greenlet-0.4.16.tar.gz", hash = "sha256:6e06eac722676797e8fce4adb8ad3dc57a1bb3adfb0dd3fdf8306c055a38456c"},
]
h11 = [
{file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"},
{file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"},
@@ -732,8 +1073,12 @@ immutables = [
{file = "immutables-0.14.tar.gz", hash = "sha256:a0a1cc238b678455145bae291d8426f732f5255537ed6a5b7645949704c70a78"},
]
importlib-metadata = [
{file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"},
{file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"},
{file = "importlib_metadata-1.6.1-py2.py3-none-any.whl", hash = "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"},
{file = "importlib_metadata-1.6.1.tar.gz", hash = "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545"},
]
itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
]
jinja2 = [
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
@@ -743,6 +1088,10 @@ jmespath = [
{file = "jmespath-0.9.5-py2.py3-none-any.whl", hash = "sha256:695cb76fa78a10663425d5b73ddc5714eb711157e52704d69be03b1a02ba4fec"},
{file = "jmespath-0.9.5.tar.gz", hash = "sha256:cca55c8d153173e21baa59983015ad0daf603f9cb799904ff057bfb8ff8dc2d9"},
]
locust = [
{file = "locust-1.0.3-py3-none-any.whl", hash = "sha256:b80deea54f470d8fd61a8f610d87041ce908924fe3fca09700376de49aa89195"},
{file = "locust-1.0.3.tar.gz", hash = "sha256:7c2e850a84275b6f7c5ad4a8abde3488df94c08d3b52e619d9c71cc6b8c853cb"},
]
loguru = [
{file = "loguru-0.4.1-py3-none-any.whl", hash = "sha256:074b3caa6748452c1e4f2b302093c94b65d5a4c5a4d7743636b4121e06437b0e"},
{file = "loguru-0.4.1.tar.gz", hash = "sha256:a6101fd435ac89ba5205a105a26a6ede9e4ddbb4408a6e167852efca47806d11"},
@@ -778,8 +1127,28 @@ markupsafe = [
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
]
more-itertools = [
{file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"},
{file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"},
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
{file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
]
msgpack = [
{file = "msgpack-1.0.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08"},
{file = "msgpack-1.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be"},
{file = "msgpack-1.0.0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a"},
{file = "msgpack-1.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf"},
{file = "msgpack-1.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8"},
{file = "msgpack-1.0.0-cp36-cp36m-win32.whl", hash = "sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1"},
{file = "msgpack-1.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2"},
{file = "msgpack-1.0.0-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97"},
{file = "msgpack-1.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e"},
{file = "msgpack-1.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140"},
{file = "msgpack-1.0.0-cp37-cp37m-win32.whl", hash = "sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272"},
{file = "msgpack-1.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322"},
{file = "msgpack-1.0.0-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab"},
{file = "msgpack-1.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84"},
{file = "msgpack-1.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e"},
{file = "msgpack-1.0.0-cp38-cp38-win32.whl", hash = "sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408"},
{file = "msgpack-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d"},
{file = "msgpack-1.0.0.tar.gz", hash = "sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0"},
]
packaging = [
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
@@ -793,9 +1162,26 @@ pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
psutil = [
{file = "psutil-5.7.0-cp27-none-win32.whl", hash = "sha256:298af2f14b635c3c7118fd9183843f4e73e681bb6f01e12284d4d70d48a60953"},
{file = "psutil-5.7.0-cp27-none-win_amd64.whl", hash = "sha256:75e22717d4dbc7ca529ec5063000b2b294fc9a367f9c9ede1f65846c7955fd38"},
{file = "psutil-5.7.0-cp35-cp35m-win32.whl", hash = "sha256:f344ca230dd8e8d5eee16827596f1c22ec0876127c28e800d7ae20ed44c4b310"},
{file = "psutil-5.7.0-cp35-cp35m-win_amd64.whl", hash = "sha256:e2d0c5b07c6fe5a87fa27b7855017edb0d52ee73b71e6ee368fae268605cc3f5"},
{file = "psutil-5.7.0-cp36-cp36m-win32.whl", hash = "sha256:a02f4ac50d4a23253b68233b07e7cdb567bd025b982d5cf0ee78296990c22d9e"},
{file = "psutil-5.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1413f4158eb50e110777c4f15d7c759521703bd6beb58926f1d562da40180058"},
{file = "psutil-5.7.0-cp37-cp37m-win32.whl", hash = "sha256:d008ddc00c6906ec80040d26dc2d3e3962109e40ad07fd8a12d0284ce5e0e4f8"},
{file = "psutil-5.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:73f35ab66c6c7a9ce82ba44b1e9b1050be2a80cd4dcc3352cc108656b115c74f"},
{file = "psutil-5.7.0-cp38-cp38-win32.whl", hash = "sha256:60b86f327c198561f101a92be1995f9ae0399736b6eced8f24af41ec64fb88d4"},
{file = "psutil-5.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:d84029b190c8a66a946e28b4d3934d2ca1528ec94764b180f7d6ea57b0e75e26"},
{file = "psutil-5.7.0.tar.gz", hash = "sha256:685ec16ca14d079455892f25bd124df26ff9137664af445563c1bd36629b5e0e"},
]
py = [
{file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"},
{file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"},
{file = "py-1.8.2-py2.py3-none-any.whl", hash = "sha256:a673fa23d7000440cc885c17dbd34fafcb7d7a6e230b29f6766400de36a33c44"},
{file = "py-1.8.2.tar.gz", hash = "sha256:f3b3a4c36512a4c4f024041ab51866f11761cc169670204b235f6b20523d4e6b"},
]
pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
]
pydantic = [
{file = "pydantic-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2a6904e9f18dea58f76f16b95cba6a2f20b72d787abd84ecd67ebc526e61dce6"},
@@ -821,8 +1207,8 @@ pyparsing = [
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pytest = [
{file = "pytest-5.4.2-py3-none-any.whl", hash = "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3"},
{file = "pytest-5.4.2.tar.gz", hash = "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"},
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
]
pytest-html = [
{file = "pytest-html-2.1.1.tar.gz", hash = "sha256:6a4ac391e105e391208e3eb9bd294a60dd336447fd8e1acddff3a6de7f4e57c5"},
@@ -845,32 +1231,62 @@ pyyaml = [
{file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"},
{file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"},
]
pyzmq = [
{file = "pyzmq-19.0.1-cp27-cp27m-macosx_10_9_intel.whl", hash = "sha256:58688a2dfa044fad608a8e70ba8d019d0b872ec2acd75b7b5e37da8905605891"},
{file = "pyzmq-19.0.1-cp27-cp27m-win32.whl", hash = "sha256:87c78f6936e2654397ca2979c1d323ee4a889eef536cc77a938c6b5be33351a7"},
{file = "pyzmq-19.0.1-cp27-cp27m-win_amd64.whl", hash = "sha256:97b6255ae77328d0e80593681826a0479cb7bac0ba8251b4dd882f5145a2293a"},
{file = "pyzmq-19.0.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:15b4cb21118f4589c4db8be4ac12b21c8b4d0d42b3ee435d47f686c32fe2e91f"},
{file = "pyzmq-19.0.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:931339ac2000d12fe212e64f98ce291e81a7ec6c73b125f17cf08415b753c087"},
{file = "pyzmq-19.0.1-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:2a88b8fabd9cc35bd59194a7723f3122166811ece8b74018147a4ed8489e6421"},
{file = "pyzmq-19.0.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:bafd651b557dd81d89bd5f9c678872f3e7b7255c1c751b78d520df2caac80230"},
{file = "pyzmq-19.0.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8952f6ba6ae598e792703f3134af5a01af8f5c7cf07e9a148f05a12b02412cea"},
{file = "pyzmq-19.0.1-cp35-cp35m-win32.whl", hash = "sha256:54aa24fd60c4262286fc64ca632f9e747c7cc3a3a1144827490e1dc9b8a3a960"},
{file = "pyzmq-19.0.1-cp35-cp35m-win_amd64.whl", hash = "sha256:dcbc3f30c11c60d709c30a213dc56e88ac016fe76ac6768e64717bd976072566"},
{file = "pyzmq-19.0.1-cp36-cp36m-macosx_10_9_intel.whl", hash = "sha256:6ca519309703e95d55965735a667809bbb65f52beda2fdb6312385d3e7a6d234"},
{file = "pyzmq-19.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4ee0bfd82077a3ff11c985369529b12853a4064320523f8e5079b630f9551448"},
{file = "pyzmq-19.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ba6f24431b569aec674ede49cad197cad59571c12deed6ad8e3c596da8288217"},
{file = "pyzmq-19.0.1-cp36-cp36m-win32.whl", hash = "sha256:956775444d01331c7eb412c5fb9bb62130dfaac77e09f32764ea1865234e2ca9"},
{file = "pyzmq-19.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b08780e3a55215873b3b8e6e7ca8987f14c902a24b6ac081b344fd430d6ca7cd"},
{file = "pyzmq-19.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21f7d91f3536f480cb2c10d0756bfa717927090b7fb863e6323f766e5461ee1c"},
{file = "pyzmq-19.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bfff5ffff051f5aa47ba3b379d87bd051c3196b0c8a603e8b7ed68a6b4f217ec"},
{file = "pyzmq-19.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:07fb8fe6826a229dada876956590135871de60dbc7de5a18c3bcce2ed1f03c98"},
{file = "pyzmq-19.0.1-cp37-cp37m-win32.whl", hash = "sha256:342fb8a1dddc569bc361387782e8088071593e7eaf3e3ecf7d6bd4976edff112"},
{file = "pyzmq-19.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:faee2604f279d31312bc455f3d024f160b6168b9c1dde22bf62d8c88a4deca8e"},
{file = "pyzmq-19.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b9d21fc56c8aacd2e6d14738021a9d64f3f69b30578a99325a728e38a349f85"},
{file = "pyzmq-19.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af0c02cf49f4f9eedf38edb4f3b6bb621d83026e7e5d76eb5526cc5333782fd6"},
{file = "pyzmq-19.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5f1f2eb22aab606f808163eb1d537ac9a0ba4283fbeb7a62eb48d9103cf015c2"},
{file = "pyzmq-19.0.1-cp38-cp38-win32.whl", hash = "sha256:f9d7e742fb0196992477415bb34366c12e9bb9a0699b8b3f221ff93b213d7bec"},
{file = "pyzmq-19.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:5b99c2ae8089ef50223c28bac57510c163bfdff158c9e90764f812b94e69a0e6"},
{file = "pyzmq-19.0.1-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:cf5d689ba9513b9753959164cf500079383bc18859f58bf8ce06d8d4bef2b054"},
{file = "pyzmq-19.0.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:aaa8b40b676576fd7806839a5de8e6d5d1b74981e6376d862af6c117af2a3c10"},
{file = "pyzmq-19.0.1.tar.gz", hash = "sha256:13a5638ab24d628a6ade8f794195e1a1acd573496c3b85af2f1183603b7bf5e0"},
]
regex = [
{file = "regex-2020.5.14-cp27-cp27m-win32.whl", hash = "sha256:e565569fc28e3ba3e475ec344d87ed3cd8ba2d575335359749298a0899fe122e"},
{file = "regex-2020.5.14-cp27-cp27m-win_amd64.whl", hash = "sha256:d466967ac8e45244b9dfe302bbe5e3337f8dc4dec8d7d10f5e950d83b140d33a"},
{file = "regex-2020.5.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:27ff7325b297fb6e5ebb70d10437592433601c423f5acf86e5bc1ee2919b9561"},
{file = "regex-2020.5.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ea55b80eb0d1c3f1d8d784264a6764f931e172480a2f1868f2536444c5f01e01"},
{file = "regex-2020.5.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c9bce6e006fbe771a02bda468ec40ffccbf954803b470a0345ad39c603402577"},
{file = "regex-2020.5.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:d881c2e657c51d89f02ae4c21d9adbef76b8325fe4d5cf0e9ad62f850f3a98fd"},
{file = "regex-2020.5.14-cp36-cp36m-win32.whl", hash = "sha256:99568f00f7bf820c620f01721485cad230f3fb28f57d8fbf4a7967ec2e446994"},
{file = "regex-2020.5.14-cp36-cp36m-win_amd64.whl", hash = "sha256:70c14743320a68c5dac7fc5a0f685be63bc2024b062fe2aaccc4acc3d01b14a1"},
{file = "regex-2020.5.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a7c37f048ec3920783abab99f8f4036561a174f1314302ccfa4e9ad31cb00eb4"},
{file = "regex-2020.5.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:89d76ce33d3266173f5be80bd4efcbd5196cafc34100fdab814f9b228dee0fa4"},
{file = "regex-2020.5.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:51f17abbe973c7673a61863516bdc9c0ef467407a940f39501e786a07406699c"},
{file = "regex-2020.5.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ce5cc53aa9fbbf6712e92c7cf268274eaff30f6bd12a0754e8133d85a8fb0f5f"},
{file = "regex-2020.5.14-cp37-cp37m-win32.whl", hash = "sha256:8044d1c085d49673aadb3d7dc20ef5cb5b030c7a4fa253a593dda2eab3059929"},
{file = "regex-2020.5.14-cp37-cp37m-win_amd64.whl", hash = "sha256:c2062c7d470751b648f1cacc3f54460aebfc261285f14bc6da49c6943bd48bdd"},
{file = "regex-2020.5.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:329ba35d711e3428db6b45a53b1b13a0a8ba07cbbcf10bbed291a7da45f106c3"},
{file = "regex-2020.5.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:579ea215c81d18da550b62ff97ee187b99f1b135fd894a13451e00986a080cad"},
{file = "regex-2020.5.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:3a9394197664e35566242686d84dfd264c07b20f93514e2e09d3c2b3ffdf78fe"},
{file = "regex-2020.5.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ce367d21f33e23a84fb83a641b3834dd7dd8e9318ad8ff677fbfae5915a239f7"},
{file = "regex-2020.5.14-cp38-cp38-win32.whl", hash = "sha256:1386e75c9d1574f6aa2e4eb5355374c8e55f9aac97e224a8a5a6abded0f9c927"},
{file = "regex-2020.5.14-cp38-cp38-win_amd64.whl", hash = "sha256:7e61be8a2900897803c293247ef87366d5df86bf701083b6c43119c7c6c99108"},
{file = "regex-2020.5.14.tar.gz", hash = "sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5"},
{file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"},
{file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"},
{file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"},
{file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"},
{file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"},
{file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"},
{file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"},
{file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"},
{file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"},
{file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"},
{file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"},
{file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"},
{file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"},
{file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"},
{file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"},
{file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"},
{file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"},
{file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"},
{file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"},
{file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"},
{file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"},
]
requests = [
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
{file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
{file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
]
requests-toolbelt = [
{file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
@@ -934,8 +1350,8 @@ uvloop = [
{file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"},
]
wcwidth = [
{file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"},
{file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"},
{file = "wcwidth-0.2.4-py2.py3-none-any.whl", hash = "sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f"},
{file = "wcwidth-0.2.4.tar.gz", hash = "sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f"},
]
websockets = [
{file = "websockets-8.0.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:e906128532a14b9d264a43eb48f9b3080d53a9bda819ab45bf56b8039dc606ac"},
@@ -950,6 +1366,10 @@ websockets = [
{file = "websockets-8.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:049e694abe33f8a1d99969fee7bfc0ae6761f7fd5f297c58ea933b27dd6805f2"},
{file = "websockets-8.0.2.tar.gz", hash = "sha256:882a7266fa867a2ebb2c0baaa0f9159cabf131cf18c1b4270d79ad42f9208dc5"},
]
werkzeug = [
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},
{file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"},
]
win32-setctime = [
{file = "win32_setctime-1.0.1-py3-none-any.whl", hash = "sha256:568fd636c68350bcc54755213fe01966fe0a6c90b386c0776425944a0382abef"},
{file = "win32_setctime-1.0.1.tar.gz", hash = "sha256:b47e5023ec7f0b4962950902b15bc56464a380d869f59d27dbf9ab423b23e8f9"},
@@ -958,3 +1378,49 @@ zipp = [
{file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},
{file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"},
]
"zope.event" = [
{file = "zope.event-4.4-py2.py3-none-any.whl", hash = "sha256:d8e97d165fd5a0997b45f5303ae11ea3338becfe68c401dd88ffd2113fe5cae7"},
{file = "zope.event-4.4.tar.gz", hash = "sha256:69c27debad9bdacd9ce9b735dad382142281ac770c4a432b533d6d65c4614bcf"},
]
"zope.interface" = [
{file = "zope.interface-5.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd"},
{file = "zope.interface-5.1.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8"},
{file = "zope.interface-5.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813"},
{file = "zope.interface-5.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086"},
{file = "zope.interface-5.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7"},
{file = "zope.interface-5.1.0-cp27-cp27m-win32.whl", hash = "sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9"},
{file = "zope.interface-5.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe"},
{file = "zope.interface-5.1.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b"},
{file = "zope.interface-5.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425"},
{file = "zope.interface-5.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8"},
{file = "zope.interface-5.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda"},
{file = "zope.interface-5.1.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d"},
{file = "zope.interface-5.1.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6"},
{file = "zope.interface-5.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08"},
{file = "zope.interface-5.1.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e"},
{file = "zope.interface-5.1.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e"},
{file = "zope.interface-5.1.0-cp35-cp35m-win32.whl", hash = "sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9"},
{file = "zope.interface-5.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578"},
{file = "zope.interface-5.1.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f"},
{file = "zope.interface-5.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975"},
{file = "zope.interface-5.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58"},
{file = "zope.interface-5.1.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345"},
{file = "zope.interface-5.1.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0"},
{file = "zope.interface-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc"},
{file = "zope.interface-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5"},
{file = "zope.interface-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d"},
{file = "zope.interface-5.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34"},
{file = "zope.interface-5.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a"},
{file = "zope.interface-5.1.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826"},
{file = "zope.interface-5.1.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5"},
{file = "zope.interface-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd"},
{file = "zope.interface-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11"},
{file = "zope.interface-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c"},
{file = "zope.interface-5.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286"},
{file = "zope.interface-5.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc"},
{file = "zope.interface-5.1.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee"},
{file = "zope.interface-5.1.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19"},
{file = "zope.interface-5.1.0-cp38-cp38-win32.whl", hash = "sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5"},
{file = "zope.interface-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a"},
{file = "zope.interface-5.1.0.tar.gz", hash = "sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e"},
]

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "httprunner"
version = "3.0.13"
version = "3.1.0"
description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0"
readme = "README.md"
@@ -42,10 +42,12 @@ sentry-sdk = "^0.14.4"
allure-pytest = {version = "^2.8.16", optional = true}
requests-toolbelt = {version = "^0.9.1", optional = true}
filetype = {version = "^1.0.7", optional = true}
locust = {version = "^1.0.3", optional = true}
[tool.poetry.extras]
allure = ["allure-pytest"] # pip install "httprunner[allure]", poetry install -E allure
upload = ["requests-toolbelt", "filetype"] # pip install "httprunner[upload]", poetry install -E upload
locust = ["locust"] # pip install "httprunner[locust]", poetry install -E locust
[tool.poetry.dev-dependencies]
coverage = "^4.5.4"
@@ -57,6 +59,7 @@ httprunner = "httprunner.cli:main"
hrun = "httprunner.cli:main_hrun_alias"
hmake = "httprunner.cli:main_make_alias"
har2case = "httprunner.cli:main_har2case_alias"
locusts = "httprunner.ext.locust:main_locusts"
[build-system]
requires = ["poetry>=1.0.0"]

View File

@@ -1,4 +1,5 @@
import io
import os
import sys
import unittest
@@ -40,10 +41,12 @@ class TestCli(unittest.TestCase):
self.assertIn(__description__, self.captured_output.getvalue().strip())
def test_debug_pytest(self):
exit_code = pytest.main(
[
"-s",
"examples/postman_echo/request_methods/request_with_testcase_reference_test.py",
]
)
self.assertEqual(exit_code, 0)
cwd = os.getcwd()
try:
os.chdir(os.path.join(cwd, "examples", "postman_echo"))
exit_code = pytest.main(
["-s", "request_methods/request_with_testcase_reference_test.py",]
)
self.assertEqual(exit_code, 0)
finally:
os.chdir(cwd)

View File

@@ -2,7 +2,6 @@ import os
import unittest
from httprunner import compat, exceptions, loader
from httprunner.compat import convert_variables, ensure_path_sep
class TestCompat(unittest.TestCase):
@@ -12,72 +11,72 @@ class TestCompat(unittest.TestCase):
def test_convert_variables(self):
raw_variables = [{"var1": 1}, {"var2": "val2"}]
self.assertEqual(
convert_variables(raw_variables, "tests/data/a-b.c/1.yml"),
compat.convert_variables(raw_variables, "tests/data/a-b.c/1.yml"),
{"var1": 1, "var2": "val2"},
)
raw_variables = {"var1": 1, "var2": "val2"}
self.assertEqual(
convert_variables(raw_variables, "tests/data/a-b.c/1.yml"),
compat.convert_variables(raw_variables, "tests/data/a-b.c/1.yml"),
{"var1": 1, "var2": "val2"},
)
raw_variables = "${get_variables()}"
self.assertEqual(
convert_variables(raw_variables, "tests/data/a-b.c/1.yml"),
compat.convert_variables(raw_variables, "tests/data/a-b.c/1.yml"),
{"foo1": "session_bar1"},
)
with self.assertRaises(exceptions.TestCaseFormatError):
raw_variables = [{"var1": 1}, {"var2": "val2", "var3": 3}]
convert_variables(raw_variables, "tests/data/a-b.c/1.yml")
compat.convert_variables(raw_variables, "tests/data/a-b.c/1.yml")
with self.assertRaises(exceptions.TestCaseFormatError):
convert_variables(None, "tests/data/a-b.c/1.yml")
compat.convert_variables(None, "tests/data/a-b.c/1.yml")
def test_convert_jmespath(self):
self.assertEqual(compat.convert_jmespath("content.abc"), "body.abc")
self.assertEqual(compat.convert_jmespath("json.abc"), "body.abc")
self.assertEqual(compat._convert_jmespath("content.abc"), "body.abc")
self.assertEqual(compat._convert_jmespath("json.abc"), "body.abc")
self.assertEqual(
compat.convert_jmespath("headers.Content-Type"), 'headers."Content-Type"'
compat._convert_jmespath("headers.Content-Type"), 'headers."Content-Type"'
)
self.assertEqual(
compat.convert_jmespath('headers."Content-Type"'), 'headers."Content-Type"'
compat._convert_jmespath('headers."Content-Type"'), 'headers."Content-Type"'
)
self.assertEqual(
compat.convert_jmespath("body.data.buildings.0.building_id"),
compat._convert_jmespath("body.data.buildings.0.building_id"),
"body.data.buildings[0].building_id",
)
with self.assertRaises(SystemExit):
compat.convert_jmespath("2.buildings.0.building_id")
compat._convert_jmespath("2.buildings.0.building_id")
def test_convert_extractors(self):
self.assertEqual(
compat.convert_extractors(
compat._convert_extractors(
[{"varA": "content.varA"}, {"varB": "json.varB"}]
),
{"varA": "body.varA", "varB": "body.varB"},
)
self.assertEqual(
compat.convert_extractors([{"varA": "content.0.varA"}]),
compat._convert_extractors([{"varA": "content.0.varA"}]),
{"varA": "body[0].varA"},
)
self.assertEqual(
compat.convert_extractors({"varA": "content.0.varA"}),
compat._convert_extractors({"varA": "content.0.varA"}),
{"varA": "body[0].varA"},
)
def test_convert_validators(self):
self.assertEqual(
compat.convert_validators(
compat._convert_validators(
[{"check": "content.abc", "assert": "eq", "expect": 201}]
),
[{"check": "body.abc", "assert": "eq", "expect": 201}],
)
self.assertEqual(
compat.convert_validators([{"eq": ["content.abc", 201]}]),
compat._convert_validators([{"eq": ["content.abc", 201]}]),
[{"eq": ["body.abc", 201]}],
)
self.assertEqual(
compat.convert_validators([{"eq": ["content.0.name", 201]}]),
compat._convert_validators([{"eq": ["content.0.name", 201]}]),
[{"eq": ["body[0].name", 201]}],
)
@@ -216,16 +215,16 @@ class TestCompat(unittest.TestCase):
def test_ensure_file_path(self):
self.assertEqual(
ensure_path_sep("demo\\test.yml"), os.sep.join(["demo", "test.yml"])
compat.ensure_path_sep("demo\\test.yml"), os.sep.join(["demo", "test.yml"])
)
self.assertEqual(
ensure_path_sep(os.path.join(os.getcwd(), "demo\\test.yml")),
compat.ensure_path_sep(os.path.join(os.getcwd(), "demo\\test.yml")),
os.path.join(os.getcwd(), os.sep.join(["demo", "test.yml"])),
)
self.assertEqual(
ensure_path_sep("demo/test.yml"), os.sep.join(["demo", "test.yml"])
compat.ensure_path_sep("demo/test.yml"), os.sep.join(["demo", "test.yml"])
)
self.assertEqual(
ensure_path_sep(os.path.join(os.getcwd(), "demo/test.yml")),
compat.ensure_path_sep(os.path.join(os.getcwd(), "demo/test.yml")),
os.path.join(os.getcwd(), os.sep.join(["demo", "test.yml"])),
)

View File

@@ -6,7 +6,7 @@ config:
teststeps:
-
name: request with functions
testcase: 1.yml
testcase: a-b.c/1.yml
export:
- session_foo2
-

View File

View File

@@ -8,6 +8,7 @@ from httprunner.make import (
make_config_chain_style,
make_teststep_chain_style,
pytest_files_run_set,
ensure_file_abs_path_valid,
)
from httprunner import loader
@@ -64,7 +65,7 @@ class TestMake(unittest.TestCase):
content = f.read()
self.assertIn(
"""
from examples.postman_echo.request_methods.request_with_functions_test import (
from request_methods.request_with_functions_test import (
TestCaseRequestWithFunctions as RequestWithFunctions,
)
""",
@@ -90,47 +91,57 @@ from examples.postman_echo.request_methods.request_with_functions_test import (
testcase_python_list,
)
def test_ensure_file_path_valid(self):
self.assertEqual(
ensure_file_abs_path_valid(
os.path.join(os.getcwd(), "tests", "data", "a-b.c", "2 3.yml")
),
os.path.join(os.getcwd(), "tests", "data", "a_b_c", "T2_3.yml"),
)
loader.project_meta = None
self.assertEqual(
ensure_file_abs_path_valid(
os.path.join(os.getcwd(), "examples", "postman_echo", "request_methods")
),
os.path.join(os.getcwd(), "examples", "postman_echo", "request_methods"),
)
loader.project_meta = None
self.assertEqual(
ensure_file_abs_path_valid(os.path.join(os.getcwd(), "README.md")),
os.path.join(os.getcwd(), "README.md"),
)
loader.project_meta = None
self.assertEqual(
ensure_file_abs_path_valid(os.getcwd()), os.getcwd(),
)
loader.project_meta = None
self.assertEqual(
ensure_file_abs_path_valid(
os.path.join(os.getcwd(), "tests", "data", ".csv")
),
os.path.join(os.getcwd(), "tests", "data", ".csv"),
)
def test_convert_testcase_path(self):
self.assertEqual(
convert_testcase_path("mubu.login.yml"),
(os.path.join(os.getcwd(), "mubu_login_test.py"), "MubuLogin"),
convert_testcase_path(
os.path.join(os.getcwd(), "tests", "data", "a-b.c", "2 3.yml")
),
(
os.path.join(os.getcwd(), "tests", "data", "a_b_c", "T2_3_test.py"),
"T23",
),
)
self.assertEqual(
convert_testcase_path(
os.path.join(os.getcwd(), os.path.join("path", "to", "mubu.login.yml"))
os.path.join(os.getcwd(), "tests", "data", "a-b.c", "中文case.yml")
),
(
os.path.join(
os.getcwd(), os.path.join("path", "to", "mubu_login_test.py")
os.getcwd(),
os.path.join("tests", "data", "a_b_c", "中文case_test.py"),
),
"MubuLogin",
),
)
self.assertEqual(
convert_testcase_path(os.path.join("path", "to 2", "mubu.login.yml")),
(
os.path.join(
os.getcwd(), os.path.join("path", "to_2", "mubu_login_test.py")
),
"MubuLogin",
),
)
self.assertEqual(
convert_testcase_path(os.path.join("path", "to-2", "mubu login.yml")),
(
os.path.join(
os.getcwd(), os.path.join("path", "to_2", "mubu_login_test.py")
),
"MubuLogin",
),
)
self.assertEqual(
convert_testcase_path(os.path.join("path", "to.2", "幕布login.yml")),
(
os.path.join(
os.getcwd(), os.path.join("path", "to_2", "幕布login_test.py")
),
"幕布Login",
"中文Case",
),
)

View File

@@ -35,6 +35,6 @@ class TestHttpRunner(unittest.TestCase):
exit_code = main_run(["tests/data/a-b.c/2 3.yml"])
self.assertEqual(exit_code, 0)
self.assertTrue(os.path.exists("tests/data/a_b_c/__init__.py"))
self.assertTrue(os.path.exists("tests/data/a_b_c/debugtalk.py"))
self.assertTrue(os.path.exists("tests/data/debugtalk.py"))
self.assertTrue(os.path.exists("tests/data/a_b_c/T1_test.py"))
self.assertTrue(os.path.exists("tests/data/a_b_c/T2_3_test.py"))

View File

@@ -5,7 +5,6 @@ import unittest
from httprunner import loader, utils
from httprunner.utils import (
ensure_file_path_valid,
ExtendJSONEncoder,
override_config_variables,
)
@@ -105,41 +104,6 @@ class TestUtils(unittest.TestCase):
["A", "D", "C", "B"],
)
def test_ensure_file_path_valid(self):
self.assertEqual(
ensure_file_path_valid(
os.path.join("examples", "a-b.c", "d f", "hardcode.yml")
),
os.path.join(os.getcwd(), "examples", "a_b_c", "d_f", "hardcode.yml"),
)
self.assertEqual(
ensure_file_path_valid(os.path.join("1", "2B", "3.yml")),
os.path.join(os.getcwd(), "T1", "T2B", "T3.yml"),
)
self.assertEqual(
ensure_file_path_valid(
os.path.join("examples", "a-b.c", "2B", "hardcode.yml")
),
os.path.join(os.getcwd(), "examples", "a_b_c", "T2B", "hardcode.yml"),
)
self.assertEqual(
ensure_file_path_valid(
os.path.join("examples", "postman_echo", "request_methods")
),
os.path.join(os.getcwd(), "examples", "postman_echo", "request_methods"),
)
self.assertEqual(
ensure_file_path_valid(os.path.join(os.getcwd(), "test.yml")),
os.path.join(os.getcwd(), "test.yml"),
)
self.assertEqual(
ensure_file_path_valid(os.getcwd()), os.getcwd(),
)
self.assertEqual(
ensure_file_path_valid(os.path.join(os.getcwd(), "demo", ".csv")),
os.path.join(os.getcwd(), "demo", ".csv"),
)
def test_safe_dump_json(self):
class A(object):
pass