From f4860de5adc4308c78c9823138a41b85dba1491d Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Wed, 5 Feb 2025 21:32:44 +0800 Subject: [PATCH] init: move from httprunner/httprunner --- .gitignore | 46 + LICENSE | 201 +++ examples/__init__.py | 0 examples/data/.csv | 0 examples/data/a-b.c/1.yml | 30 + examples/data/a-b.c/2 3.yml | 26 + examples/data/a-b.c/中文case.yml | 0 examples/data/a_b_c/T1_test.py | 34 + examples/data/a_b_c/T2_3_test.py | 44 + examples/data/a_b_c/__init__.py | 1 + examples/data/curl/curl_examples.txt | 11 + examples/data/debugtalk.py | 13 + examples/data/har/demo.har | 356 ++++ examples/data/postman/__init__.py | 1 + examples/data/postman/intro.txt | 1 + examples/data/postman/logo.jpeg | Bin 0 -> 323158 bytes examples/data/postman/postman_collection.json | 498 ++++++ examples/data/profile.yml | 4 + examples/data/profile_override.yml | 5 + examples/data/sqlite.db | Bin 0 -> 12288 bytes examples/httpbin/__init__.py | 1 + examples/httpbin/account.csv | 4 + examples/httpbin/basic.yml | 89 + examples/httpbin/basic_test.py | 79 + examples/httpbin/debugtalk.py | 148 ++ examples/httpbin/hooks.yml | 36 + examples/httpbin/hooks_test.py | 35 + examples/httpbin/load_image.yml | 37 + examples/httpbin/load_image_test.py | 39 + examples/httpbin/test.env | 4 + examples/httpbin/upload.yml | 30 + examples/httpbin/upload_test.py | 38 + examples/httpbin/user_agent.csv | 4 + examples/httpbin/validate.yml | 35 + examples/httpbin/validate_test.py | 31 + examples/postman_echo/.debugtalk_gen.py | 20 + examples/postman_echo/__init__.py | 0 examples/postman_echo/conftest.py | 65 + .../cookie_manipulation/__init__.py | 1 + .../cookie_manipulation/hardcode.yml | 34 + .../cookie_manipulation/hardcode_test.py | 41 + .../set_delete_cookies.yml | 41 + .../set_delete_cookies_test.py | 44 + examples/postman_echo/debugtalk.py | 42 + .../postman_echo/request_methods/__init__.py | 1 + .../postman_echo/request_methods/account.csv | 4 + .../postman_echo/request_methods/conftest.py | 61 + .../postman_echo/request_methods/hardcode.yml | 55 + .../request_methods/hardcode_test.py | 68 + .../request_with_functions.yml | 69 + .../request_with_functions_test.py | 84 + .../request_with_parameters.yml | 33 + .../request_with_parameters_test.py | 53 + .../request_with_retry_test.py | 31 + .../request_with_testcase_reference.yml | 37 + .../request_with_testcase_reference_test.py | 62 + .../request_with_variables.yml | 78 + .../request_with_variables_test.py | 86 + .../validate_with_functions.yml | 29 + .../validate_with_functions_test.py | 34 + .../validate_with_variables.yml | 58 + .../validate_with_variables_test.py | 66 + examples/pytest.ini | 6 + examples/sql/test_sql_demo.py | 36 + httprunner/README.md | 115 ++ httprunner/__init__.py | 38 + httprunner/__main__.py | 5 + httprunner/builtin/__init__.py | 2 + httprunner/builtin/comparators.py | 129 ++ httprunner/builtin/functions.py | 35 + httprunner/cli.py | 152 ++ httprunner/cli_test.py | 62 + httprunner/client.py | 238 +++ httprunner/client_test.py | 73 + httprunner/compat.py | 385 +++++ httprunner/compat_test.py | 266 +++ httprunner/config.py | 138 ++ httprunner/database/engine.py | 86 + httprunner/exceptions.py | 92 ++ httprunner/ext/__init__.py | 2 + httprunner/ext/uploader/__init__.py | 178 ++ httprunner/loader.py | 432 +++++ httprunner/loader_test.py | 127 ++ httprunner/make.py | 574 +++++++ httprunner/make_test.py | 213 +++ httprunner/models.py | 305 ++++ httprunner/parser.py | 606 +++++++ httprunner/parser_test.py | 574 +++++++ httprunner/response.py | 309 ++++ httprunner/response_test.py | 90 + httprunner/runner.py | 248 +++ httprunner/step.py | 67 + httprunner/step_request.py | 499 ++++++ httprunner/step_request_test.py | 17 + httprunner/step_sql_request.py | 317 ++++ httprunner/step_testcase.py | 103 ++ httprunner/step_testcase_test.py | 27 + httprunner/step_thrift_request.py | 309 ++++ httprunner/thrift/data_convertor.py | 471 ++++++ httprunner/thrift/thrift_client.py | 139 ++ httprunner/utils.py | 366 +++++ httprunner/utils_test.py | 171 ++ poetry.lock | 1447 +++++++++++++++++ pyproject.toml | 75 + 104 files changed, 12602 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 examples/__init__.py create mode 100644 examples/data/.csv create mode 100644 examples/data/a-b.c/1.yml create mode 100644 examples/data/a-b.c/2 3.yml create mode 100644 examples/data/a-b.c/中文case.yml create mode 100644 examples/data/a_b_c/T1_test.py create mode 100644 examples/data/a_b_c/T2_3_test.py create mode 100644 examples/data/a_b_c/__init__.py create mode 100644 examples/data/curl/curl_examples.txt create mode 100644 examples/data/debugtalk.py create mode 100644 examples/data/har/demo.har create mode 100644 examples/data/postman/__init__.py create mode 100644 examples/data/postman/intro.txt create mode 100644 examples/data/postman/logo.jpeg create mode 100644 examples/data/postman/postman_collection.json create mode 100644 examples/data/profile.yml create mode 100644 examples/data/profile_override.yml create mode 100644 examples/data/sqlite.db create mode 100644 examples/httpbin/__init__.py create mode 100644 examples/httpbin/account.csv create mode 100644 examples/httpbin/basic.yml create mode 100644 examples/httpbin/basic_test.py create mode 100644 examples/httpbin/debugtalk.py create mode 100644 examples/httpbin/hooks.yml create mode 100644 examples/httpbin/hooks_test.py create mode 100644 examples/httpbin/load_image.yml create mode 100644 examples/httpbin/load_image_test.py create mode 100644 examples/httpbin/test.env create mode 100644 examples/httpbin/upload.yml create mode 100644 examples/httpbin/upload_test.py create mode 100644 examples/httpbin/user_agent.csv create mode 100644 examples/httpbin/validate.yml create mode 100644 examples/httpbin/validate_test.py create mode 100644 examples/postman_echo/.debugtalk_gen.py create mode 100644 examples/postman_echo/__init__.py create mode 100644 examples/postman_echo/conftest.py create mode 100644 examples/postman_echo/cookie_manipulation/__init__.py create mode 100644 examples/postman_echo/cookie_manipulation/hardcode.yml create mode 100644 examples/postman_echo/cookie_manipulation/hardcode_test.py create mode 100644 examples/postman_echo/cookie_manipulation/set_delete_cookies.yml create mode 100644 examples/postman_echo/cookie_manipulation/set_delete_cookies_test.py create mode 100644 examples/postman_echo/debugtalk.py create mode 100644 examples/postman_echo/request_methods/__init__.py create mode 100644 examples/postman_echo/request_methods/account.csv create mode 100644 examples/postman_echo/request_methods/conftest.py create mode 100644 examples/postman_echo/request_methods/hardcode.yml create mode 100644 examples/postman_echo/request_methods/hardcode_test.py create mode 100644 examples/postman_echo/request_methods/request_with_functions.yml create mode 100644 examples/postman_echo/request_methods/request_with_functions_test.py create mode 100644 examples/postman_echo/request_methods/request_with_parameters.yml create mode 100644 examples/postman_echo/request_methods/request_with_parameters_test.py create mode 100644 examples/postman_echo/request_methods/request_with_retry_test.py create mode 100644 examples/postman_echo/request_methods/request_with_testcase_reference.yml create mode 100644 examples/postman_echo/request_methods/request_with_testcase_reference_test.py create mode 100644 examples/postman_echo/request_methods/request_with_variables.yml create mode 100644 examples/postman_echo/request_methods/request_with_variables_test.py create mode 100644 examples/postman_echo/request_methods/validate_with_functions.yml create mode 100644 examples/postman_echo/request_methods/validate_with_functions_test.py create mode 100644 examples/postman_echo/request_methods/validate_with_variables.yml create mode 100644 examples/postman_echo/request_methods/validate_with_variables_test.py create mode 100644 examples/pytest.ini create mode 100644 examples/sql/test_sql_demo.py create mode 100644 httprunner/README.md create mode 100644 httprunner/__init__.py create mode 100644 httprunner/__main__.py create mode 100644 httprunner/builtin/__init__.py create mode 100644 httprunner/builtin/comparators.py create mode 100644 httprunner/builtin/functions.py create mode 100644 httprunner/cli.py create mode 100644 httprunner/cli_test.py create mode 100644 httprunner/client.py create mode 100644 httprunner/client_test.py create mode 100644 httprunner/compat.py create mode 100644 httprunner/compat_test.py create mode 100644 httprunner/config.py create mode 100644 httprunner/database/engine.py create mode 100644 httprunner/exceptions.py create mode 100644 httprunner/ext/__init__.py create mode 100644 httprunner/ext/uploader/__init__.py create mode 100644 httprunner/loader.py create mode 100644 httprunner/loader_test.py create mode 100644 httprunner/make.py create mode 100644 httprunner/make_test.py create mode 100644 httprunner/models.py create mode 100644 httprunner/parser.py create mode 100644 httprunner/parser_test.py create mode 100644 httprunner/response.py create mode 100644 httprunner/response_test.py create mode 100644 httprunner/runner.py create mode 100644 httprunner/step.py create mode 100644 httprunner/step_request.py create mode 100644 httprunner/step_request_test.py create mode 100644 httprunner/step_sql_request.py create mode 100644 httprunner/step_testcase.py create mode 100644 httprunner/step_testcase_test.py create mode 100644 httprunner/step_thrift_request.py create mode 100644 httprunner/thrift/data_convertor.py create mode 100644 httprunner/thrift/thrift_client.py create mode 100644 httprunner/utils.py create mode 100644 httprunner/utils_test.py create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ece898 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# system or IDE generated files +__debug_bin +.vscode/ +.idea/ +.DS_Store +*.bak +.commit.txt + +# project output files +site/ +output/ +logs +*.log +*.pcap +.coverage +reports +results +*.xml +htmlcov/ +screenshots/ + +# built plugins +debugtalk.bin +debugtalk.so + +# python files +.venv +__pycache__ +*.pyc +dist +*.egg-info +.python-version +.pytest_cache diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f63cb65 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 debugtalk + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/data/.csv b/examples/data/.csv new file mode 100644 index 0000000..e69de29 diff --git a/examples/data/a-b.c/1.yml b/examples/data/a-b.c/1.yml new file mode 100644 index 0000000..5754c33 --- /dev/null +++ b/examples/data/a-b.c/1.yml @@ -0,0 +1,30 @@ +config: + name: "request methods testcase with functions" + variables: + foo1: config_bar1 + foo2: config_bar2 + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: get with params + variables: + foo1: bar1 + sum_v: "${sum_two(1, 2)}" + request: + method: GET + url: /get + params: + foo1: $foo1 + foo2: $foo2 + sum_v: $sum_v + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + extract: + session_foo2: "body.args.foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.args.foo1", "bar1"] + - eq: ["body.args.sum_v", "3"] + - eq: ["body.args.foo2", "config_bar2"] diff --git a/examples/data/a-b.c/2 3.yml b/examples/data/a-b.c/2 3.yml new file mode 100644 index 0000000..8a37b3a --- /dev/null +++ b/examples/data/a-b.c/2 3.yml @@ -0,0 +1,26 @@ +config: + name: "reference testcase unittest for abnormal folder path" + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: request with functions + testcase: a-b.c/1.yml + export: + - session_foo2 +- + name: post form data + variables: + foo1: bar12 + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + Content-Type: "application/x-www-form-urlencoded" + data: "foo1=$foo1&foo2=$session_foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "bar12"] + - eq: ["body.form.foo2", "config_bar2"] diff --git a/examples/data/a-b.c/中文case.yml b/examples/data/a-b.c/中文case.yml new file mode 100644 index 0000000..e69de29 diff --git a/examples/data/a_b_c/T1_test.py b/examples/data/a_b_c/T1_test.py new file mode 100644 index 0000000..c65163c --- /dev/null +++ b/examples/data/a_b_c/T1_test.py @@ -0,0 +1,34 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: a-b.c/1.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseT1(HttpRunner): + + config = ( + Config("request methods testcase with functions") + .variables(**{"foo1": "config_bar1", "foo2": "config_bar2"}) + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunRequest("get with params") + .with_variables(**{"foo1": "bar1", "sum_v": "${sum_two(1, 2)}"}) + .get("/get") + .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}) + .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) + .extract() + .with_jmespath("body.args.foo2", "session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.foo1", "bar1") + .assert_equal("body.args.sum_v", "3") + .assert_equal("body.args.foo2", "config_bar2") + ), + ] + + +if __name__ == "__main__": + TestCaseT1().test_start() diff --git a/examples/data/a_b_c/T2_3_test.py b/examples/data/a_b_c/T2_3_test.py new file mode 100644 index 0000000..c28c757 --- /dev/null +++ b/examples/data/a_b_c/T2_3_test.py @@ -0,0 +1,44 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: a-b.c/2 3.yml +from httprunner import HttpRunner, Config, Step, RunRequest +from httprunner import RunTestCase + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from a_b_c.T1_test import TestCaseT1 as T1 + + +class TestCaseT23(HttpRunner): + + config = ( + Config("reference testcase unittest for abnormal folder path") + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step(RunTestCase("request with functions").call(T1).export(*["session_foo2"])), + Step( + RunRequest("post form data") + .with_variables(**{"foo1": "bar12"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "bar12") + .assert_equal("body.form.foo2", "config_bar2") + ), + ] + + +if __name__ == "__main__": + TestCaseT23().test_start() diff --git a/examples/data/a_b_c/__init__.py b/examples/data/a_b_c/__init__.py new file mode 100644 index 0000000..70cfba5 --- /dev/null +++ b/examples/data/a_b_c/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! diff --git a/examples/data/curl/curl_examples.txt b/examples/data/curl/curl_examples.txt new file mode 100644 index 0000000..6d8e6a7 --- /dev/null +++ b/examples/data/curl/curl_examples.txt @@ -0,0 +1,11 @@ +curl httpbin.org + +curl https://httpbin.org/get?key1=value1&key2=value2 + +curl -H "Content-Type: application/json" -H "Authorization: Bearer b7d03a6947b217efb6f3ec3bd3504582" -d '{"type":"A","name":"www","data":"162.10.66.0","priority":null,"port":null,"weight":null}' "https://httpbin.org/post" + +curl -F "dummyName=dummyFile" -F file1=@file1.txt -F file2=@file2.txt https://httpbin.org/post + +curl https://httpbin.org/post -d 'shipment[to_address][id]=adr_HrBKVA85' -d 'shipment[from_address][id]=adr_VtuTOj7o' -d 'shipment[parcel][id]=prcl_WDv2VzHp' -d 'shipment[is_return]=true' -d 'shipment[customs_info][id]=cstinfo_bl5sE20Y' + +curl https://httpbing.org/post -H "Content-Type: application/x-www-form-urlencoded" --data "key1=value+1&key2=value%3A2" diff --git a/examples/data/debugtalk.py b/examples/data/debugtalk.py new file mode 100644 index 0000000..af8b22e --- /dev/null +++ b/examples/data/debugtalk.py @@ -0,0 +1,13 @@ +from httprunner import __version__ + + +def get_httprunner_version(): + return __version__ + + +def sum_two(m, n): + return m + n + + +def get_variables(): + return {"foo1": "session_bar1"} diff --git a/examples/data/har/demo.har b/examples/data/har/demo.har new file mode 100644 index 0000000..3a94a30 --- /dev/null +++ b/examples/data/har/demo.har @@ -0,0 +1,356 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Charles Proxy", + "version": "4.6.1" + }, + "entries": [ + { + "startedDateTime": "2021-10-15T20:29:14.396+08:00", + "time": 1528, + "request": { + "method": "GET", + "url": "https://postman-echo.com/get?foo1=HDnY8&foo2=34.5", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Host", + "value": "postman-echo.com" + }, + { + "name": "User-Agent", + "value": "HttpRunnerPlus" + }, + { + "name": "Accept-Encoding", + "value": "gzip" + } + ], + "queryString": [ + { + "name": "foo1", + "value": "HDnY8" + }, + { + "name": "foo2", + "value": "34.5" + } + ], + "headersSize": 113, + "bodySize": 0 + }, + "response": { + "_charlesStatus": "COMPLETE", + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "sails.sid", + "value": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk", + "path": "/", + "domain": null, + "expires": null, + "httpOnly": true, + "secure": false, + "comment": null, + "_maxAge": null + } + ], + "headers": [ + { + "name": "Date", + "value": "Fri, 15 Oct 2021 12:29:15 GMT" + }, + { + "name": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "name": "Content-Length", + "value": "300" + }, + { + "name": "ETag", + "value": "W/\"12c-1pyB4v4mv3hdBoU+8cUmx4p37qI\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "set-cookie", + "value": "sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk; Path=/; HttpOnly" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "content": { + "size": 300, + "mimeType": "application/json; charset=utf-8", + "text": "eyJhcmdzIjp7ImZvbzEiOiJIRG5ZOCIsImZvbzIiOiIzNC41In0sImhlYWRlcnMiOnsieC1mb3J3YXJkZWQtcHJvdG8iOiJodHRwcyIsIngtZm9yd2FyZGVkLXBvcnQiOiI0NDMiLCJob3N0IjoicG9zdG1hbi1lY2hvLmNvbSIsIngtYW16bi10cmFjZS1pZCI6IlJvb3Q9MS02MTY5NzQxYi01YjgyNTRjZTZjZThlNTU2NTRiNzc3MmQiLCJ1c2VyLWFnZW50IjoiSHR0cEJvb21lciIsImFjY2VwdC1lbmNvZGluZyI6Imd6aXAifSwidXJsIjoiaHR0cHM6Ly9wb3N0bWFuLWVjaG8uY29tL2dldD9mb28xPUhEblk4JmZvbzI9MzQuNSJ9", + "encoding": "base64" + }, + "redirectURL": null, + "headersSize": 0, + "bodySize": 300 + }, + "serverIPAddress": "44.193.31.23", + "cache": {}, + "timings": { + "dns": 105, + "connect": 1108, + "ssl": 721, + "send": 1, + "wait": 312, + "receive": 2 + } + }, + { + "startedDateTime": "2021-10-15T20:29:16.120+08:00", + "time": 306, + "request": { + "method": "POST", + "url": "https://postman-echo.com/post", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "sails.sid", + "value": "s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk" + } + ], + "headers": [ + { + "name": "Host", + "value": "postman-echo.com" + }, + { + "name": "User-Agent", + "value": "Go-http-client/1.1" + }, + { + "name": "Content-Length", + "value": "28" + }, + { + "name": "Content-Type", + "value": "application/json; charset=UTF-8" + }, + { + "name": "Cookie", + "value": "sails.sid=s%3Az_LpglkKxTvJ_eHVUH6V67drKp0AGWW-.PidabaXOnatLRP47hVyqqepl6BdrpEQzRlJQXtbIiwk" + }, + { + "name": "Accept-Encoding", + "value": "gzip" + } + ], + "queryString": [], + "postData": { + "mimeType": "application/json; charset=UTF-8", + "text": "{\"foo1\":\"HDnY8\",\"foo2\":12.3}" + }, + "headersSize": 269, + "bodySize": 28 + }, + "response": { + "_charlesStatus": "COMPLETE", + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "sails.sid", + "value": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw", + "path": "/", + "domain": null, + "expires": null, + "httpOnly": true, + "secure": false, + "comment": null, + "_maxAge": null + } + ], + "headers": [ + { + "name": "Date", + "value": "Fri, 15 Oct 2021 12:29:16 GMT" + }, + { + "name": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "name": "Content-Length", + "value": "526" + }, + { + "name": "ETag", + "value": "W/\"20e-aXqJ0H6Q30sU41c/D7asB+yXWeQ\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "set-cookie", + "value": "sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw; Path=/; HttpOnly" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "content": { + "size": 526, + "mimeType": "application/json; charset=utf-8", + "text": "eyJhcmdzIjp7fSwiZGF0YSI6eyJmb28xIjoiSERuWTgiLCJmb28yIjoxMi4zfSwiZmlsZXMiOnt9LCJmb3JtIjp7fSwiaGVhZGVycyI6eyJ4LWZvcndhcmRlZC1wcm90byI6Imh0dHBzIiwieC1mb3J3YXJkZWQtcG9ydCI6IjQ0MyIsImhvc3QiOiJwb3N0bWFuLWVjaG8uY29tIiwieC1hbXpuLXRyYWNlLWlkIjoiUm9vdD0xLTYxNjk3NDFjLTIxN2RiMGI3MWFkYjgwYmQ3ODUxOTI2OCIsImNvbnRlbnQtbGVuZ3RoIjoiMjgiLCJ1c2VyLWFnZW50IjoiR28taHR0cC1jbGllbnQvMS4xIiwiY29udGVudC10eXBlIjoiYXBwbGljYXRpb24vanNvbjsgY2hhcnNldD1VVEYtOCIsImNvb2tpZSI6InNhaWxzLnNpZD1zJTNBel9McGdsa0t4VHZKX2VIVlVINlY2N2RyS3AwQUdXVy0uUGlkYWJhWE9uYXRMUlA0N2hWeXFxZXBsNkJkcnBFUXpSbEpRWHRiSWl3ayIsImFjY2VwdC1lbmNvZGluZyI6Imd6aXAifSwianNvbiI6eyJmb28xIjoiSERuWTgiLCJmb28yIjoxMi4zfSwidXJsIjoiaHR0cHM6Ly9wb3N0bWFuLWVjaG8uY29tL3Bvc3QifQ==", + "encoding": "base64" + }, + "redirectURL": null, + "headersSize": 0, + "bodySize": 526 + }, + "serverIPAddress": "44.193.31.23", + "cache": {}, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 1, + "wait": 304, + "receive": 1 + } + }, + { + "startedDateTime": "2021-10-15T20:29:16.427+08:00", + "time": 305, + "request": { + "method": "POST", + "url": "https://postman-echo.com/post", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "sails.sid", + "value": "s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw" + } + ], + "headers": [ + { + "name": "Host", + "value": "postman-echo.com" + }, + { + "name": "User-Agent", + "value": "Go-http-client/1.1" + }, + { + "name": "Content-Length", + "value": "20" + }, + { + "name": "Content-Type", + "value": "application/x-www-form-urlencoded; charset=UTF-8" + }, + { + "name": "Cookie", + "value": "sails.sid=s%3AS5e7w0zQ0xAsCwh9L8T6R7QLYCO7_gtD.r8%2B2w9IWqEIfuVkrZjnxzm2xADIk34zKAWXRPapr%2FAw" + }, + { + "name": "Accept-Encoding", + "value": "gzip" + } + ], + "queryString": [], + "postData": { + "mimeType": "application/x-www-form-urlencoded; charset=UTF-8", + "params": [ + { + "name": "foo1", + "value": "HDnY8" + }, + { + "name": "foo2", + "value": "12.3" + } + ] + }, + "headersSize": 290, + "bodySize": 20 + }, + "response": { + "_charlesStatus": "COMPLETE", + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "sails.sid", + "value": "s%3AMp2gGgeCCDM4sRS_MfL1q-hAkL3bAk84.9XT7TTW8QzueQqtQ6bQM%2BgHqiUBbkJSfgM5CbfhFreQ", + "path": "/", + "domain": null, + "expires": null, + "httpOnly": true, + "secure": false, + "comment": null, + "_maxAge": null + } + ], + "headers": [ + { + "name": "Date", + "value": "Fri, 15 Oct 2021 12:29:16 GMT" + }, + { + "name": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "name": "Content-Length", + "value": "551" + }, + { + "name": "ETag", + "value": "W/\"227-micuvGYwtEZN542D1sTL0hAZaRs\"" + }, + { + "name": "Vary", + "value": "Accept-Encoding" + }, + { + "name": "set-cookie", + "value": "sails.sid=s%3AMp2gGgeCCDM4sRS_MfL1q-hAkL3bAk84.9XT7TTW8QzueQqtQ6bQM%2BgHqiUBbkJSfgM5CbfhFreQ; Path=/; HttpOnly" + }, + { + "name": "Connection", + "value": "keep-alive" + } + ], + "content": { + "size": 551, + "mimeType": "application/json; charset=utf-8", + "text": "eyJhcmdzIjp7fSwiZGF0YSI6IiIsImZpbGVzIjp7fSwiZm9ybSI6eyJmb28xIjoiSERuWTgiLCJmb28yIjoiMTIuMyJ9LCJoZWFkZXJzIjp7IngtZm9yd2FyZGVkLXByb3RvIjoiaHR0cHMiLCJ4LWZvcndhcmRlZC1wb3J0IjoiNDQzIiwiaG9zdCI6InBvc3RtYW4tZWNoby5jb20iLCJ4LWFtem4tdHJhY2UtaWQiOiJSb290PTEtNjE2OTc0MWMtNWI5ZDEyMWI2N2FlZTI0MTUyMmQzMjE2IiwiY29udGVudC1sZW5ndGgiOiIyMCIsInVzZXItYWdlbnQiOiJHby1odHRwLWNsaWVudC8xLjEiLCJjb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQ7IGNoYXJzZXQ9VVRGLTgiLCJjb29raWUiOiJzYWlscy5zaWQ9cyUzQVM1ZTd3MHpRMHhBc0N3aDlMOFQ2UjdRTFlDTzdfZ3RELnI4JTJCMnc5SVdxRUlmdVZrclpqbnh6bTJ4QURJazM0ektBV1hSUGFwciUyRkF3IiwiYWNjZXB0LWVuY29kaW5nIjoiZ3ppcCJ9LCJqc29uIjp7ImZvbzEiOiJIRG5ZOCIsImZvbzIiOiIxMi4zIn0sInVybCI6Imh0dHBzOi8vcG9zdG1hbi1lY2hvLmNvbS9wb3N0In0=", + "encoding": "base64" + }, + "redirectURL": null, + "headersSize": 0, + "bodySize": 551 + }, + "serverIPAddress": "44.193.31.23", + "cache": {}, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 303, + "receive": 2 + } + } + ] + } +} \ No newline at end of file diff --git a/examples/data/postman/__init__.py b/examples/data/postman/__init__.py new file mode 100644 index 0000000..70cfba5 --- /dev/null +++ b/examples/data/postman/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! diff --git a/examples/data/postman/intro.txt b/examples/data/postman/intro.txt new file mode 100644 index 0000000..1ac2b9d --- /dev/null +++ b/examples/data/postman/intro.txt @@ -0,0 +1 @@ +HttpRunner is an open source API testing tool that supports HTTP(S)/HTTP2/WebSocket/RPC network protocols, covering API testing, performance testing and digital experience monitoring (DEM) test types. Enjoy! \ No newline at end of file diff --git a/examples/data/postman/logo.jpeg b/examples/data/postman/logo.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e790a1ca084786d22eed2c9c0e0373d25c3a63e9 GIT binary patch literal 323158 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≧=dOfFjT&;xD$e0^?_$NSO5 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%0AVk&Y#gqt>joP3|hP)y`J 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

$5&q5&3W*u` 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 qj`;C?rPU61Ij&Hls_vp3fI*&}0{cgEVJg) zebVT{{--S?o1LpQ^VYH(#C|ag3IY&-00bZa0SG_<0uX=z1R(I21xc=Tl~gt3b;e8C3%@lKfsogIf`mYulCTaN_&3P|K;VBzJRW3u3?8&!(Xu z009U<00Izz00bZa0SG_<0ucC(K#k>S@sib*uYyNssG_m%C)aOh&zWUT)$zqw)|AqT zcdZ=VkXGFjqwI)@G83oD_mxTGxJGmN4RI_6cbTaq%1m{`(Krm8xM2Q?*>A-Du-{EX lK>z{}fB*y_009U<00Izz00bcL4+NGNrHm!3Wigh|e**Zte>(sG literal 0 HcmV?d00001 diff --git a/examples/httpbin/__init__.py b/examples/httpbin/__init__.py new file mode 100644 index 0000000..70cfba5 --- /dev/null +++ b/examples/httpbin/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! diff --git a/examples/httpbin/account.csv b/examples/httpbin/account.csv new file mode 100644 index 0000000..67ce22c --- /dev/null +++ b/examples/httpbin/account.csv @@ -0,0 +1,4 @@ +username,password +test1,111111 +test2,222222 +test3,333333 \ No newline at end of file diff --git a/examples/httpbin/basic.yml b/examples/httpbin/basic.yml new file mode 100644 index 0000000..69a6db7 --- /dev/null +++ b/examples/httpbin/basic.yml @@ -0,0 +1,89 @@ +config: + name: basic test with httpbin + base_url: ${get_httpbin_server()} + +teststeps: +- + name: headers + request: + url: /headers + method: GET + validate: + - eq: ["status_code", 200] + - eq: [body.headers.Host, "127.0.0.1"] + +- + name: user-agent + request: + url: /user-agent + method: GET + validate: + - eq: ["status_code", 200] + - startswith: [body."user-agent", "python-requests"] + +- + name: get without params + request: + url: /get + method: GET + validate: + - eq: ["status_code", 200] + - eq: [body.args, {}] + +- + name: get with params in url + request: + url: /get?a=1&b=2 + method: GET + validate: + - eq: ["status_code", 200] + - eq: [body.args, {'a': '1', 'b': '2'}] + +- + name: get with params in params field + request: + url: /get + params: + a: 1 + b: 2 + method: GET + validate: + - eq: ["status_code", 200] + - eq: [body.args, {'a': '1', 'b': '2'}] + +- + name: set cookie + request: + url: /cookies/set?name=value + method: GET + validate: + - eq: ["status_code", 200] + - eq: [body.cookies.name, "value"] + +- + name: extract cookie + request: + url: /cookies + method: GET + validate: + - eq: ["status_code", 200] + - eq: [body.cookies.name, "value"] + +- + name: post data + request: + url: /post + method: POST + headers: + Content-Type: application/json + data: abc + validate: + - eq: ["status_code", 200] + +- + name: validate body length + request: + url: /spec.json + method: GET + validate: + - len_eq: ["body", 9] diff --git a/examples/httpbin/basic_test.py b/examples/httpbin/basic_test.py new file mode 100644 index 0000000..1933ddf --- /dev/null +++ b/examples/httpbin/basic_test.py @@ -0,0 +1,79 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: basic.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseBasic(HttpRunner): + + config = Config("basic test with httpbin").base_url("${get_httpbin_server()}") + + teststeps = [ + Step( + RunRequest("headers") + .get("/headers") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.headers.Host", "127.0.0.1") + ), + Step( + RunRequest("user-agent") + .get("/user-agent") + .validate() + .assert_equal("status_code", 200) + .assert_startswith('body."user-agent"', "python-requests") + ), + Step( + RunRequest("get without params") + .get("/get") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args", {}) + ), + Step( + RunRequest("get with params in url") + .get("/get?a=1&b=2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args", {"a": "1", "b": "2"}) + ), + Step( + RunRequest("get with params in params field") + .get("/get") + .with_params(**{"a": 1, "b": 2}) + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args", {"a": "1", "b": "2"}) + ), + Step( + RunRequest("set cookie") + .get("/cookies/set?name=value") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.cookies.name", "value") + ), + Step( + RunRequest("extract cookie") + .get("/cookies") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.cookies.name", "value") + ), + Step( + RunRequest("post data") + .post("/post") + .with_headers(**{"Content-Type": "application/json"}) + .with_data("abc") + .validate() + .assert_equal("status_code", 200) + ), + Step( + RunRequest("validate body length") + .get("/spec.json") + .validate() + .assert_length_equal("body", 9) + ), + ] + + +if __name__ == "__main__": + TestCaseBasic().test_start() diff --git a/examples/httpbin/debugtalk.py b/examples/httpbin/debugtalk.py new file mode 100644 index 0000000..5f40844 --- /dev/null +++ b/examples/httpbin/debugtalk.py @@ -0,0 +1,148 @@ +import os +import random +import string +import time +import uuid + +from loguru import logger + +from httprunner.utils import HTTP_BIN_URL + + +def get_httpbin_server(): + return HTTP_BIN_URL + + +def setup_testcase(variables): + logger.info(f"setup_testcase, variables: {variables}") + variables["request_id_prefix"] = str(int(time.time())) + + +def teardown_testcase(): + logger.info("teardown_testcase.") + + +def setup_teststep(request, variables): + logger.info(f"setup_teststep, request: {request}, variables: {variables}") + request.setdefault("headers", {}) + request_id_prefix = variables["request_id_prefix"] + request["headers"]["HRUN-Request-ID"] = request_id_prefix + "-" + str(uuid.uuid4()) + + +def teardown_teststep(response): + logger.info(f"teardown_teststep, response status code: {response.status_code}") + + +def sum_two(m, n): + return m + n + + +def sum_status_code(status_code, expect_sum): + """sum status code digits + e.g. 400 => 4, 201 => 3 + """ + sum_value = 0 + for digit in str(status_code): + sum_value += int(digit) + + assert sum_value == expect_sum + + +def is_status_code_200(status_code): + return status_code == 200 + + +os.environ["TEST_ENV"] = "PRODUCTION" + + +def skip_test_in_production_env(): + """skip this test in production environment""" + return os.environ["TEST_ENV"] == "PRODUCTION" + + +def get_user_agent(): + return ["iOS/10.1", "iOS/10.2"] + + +def gen_app_version(): + return [{"app_version": "2.8.5"}, {"app_version": "2.8.6"}] + + +def get_account(): + return [ + {"username": "user1", "password": "111111"}, + {"username": "user2", "password": "222222"}, + ] + + +def get_account_in_tuple(): + return [("user1", "111111"), ("user2", "222222")] + + +def gen_random_string(str_len): + random_char_list = [] + for _ in range(str_len): + random_char = random.choice(string.ascii_letters + string.digits) + random_char_list.append(random_char) + + random_string = "".join(random_char_list) + return random_string + + +def setup_hook_add_kwargs(request): + request["key"] = "value" + + +def setup_hook_remove_kwargs(request): + request.pop("key") + + +def teardown_hook_sleep_N_secs(response, n_secs): + """sleep n seconds after request""" + if response.status_code == 200: + time.sleep(0.1) + else: + time.sleep(n_secs) + + +def hook_print(msg): + print(msg) + + +def modify_request_json(request, os_platform): + request["json"]["os_platform"] = os_platform + + +def setup_hook_httpntlmauth(request): + if "httpntlmauth" in request: + from requests_ntlm import HttpNtlmAuth + + auth_account = request.pop("httpntlmauth") + request["auth"] = HttpNtlmAuth( + auth_account["username"], auth_account["password"] + ) + + +def alter_response(response): + response.status_code = 500 + response.headers["Content-Type"] = "html/text" + response.body["headers"]["Host"] = "127.0.0.1:8888" + response.new_attribute = "new_attribute_value" + response.new_attribute_dict = {"key": 123} + + +def alter_response_302(response): + response.status_code = 500 + response.headers["Content-Type"] = "html/text" + response.text = "abcdef" + response.new_attribute = "new_attribute_value" + response.new_attribute_dict = {"key": 123} + + +def alter_response_error(response): + # NameError + not_defined_variable + + +def gen_variables(): + return {"var_a": 1, "var_b": 2} diff --git a/examples/httpbin/hooks.yml b/examples/httpbin/hooks.yml new file mode 100644 index 0000000..23f0ba5 --- /dev/null +++ b/examples/httpbin/hooks.yml @@ -0,0 +1,36 @@ +config: + name: basic test with httpbin + base_url: ${get_httpbin_server()} + setup_hooks: + - ${hook_print(setup)} + teardown_hooks: + - ${hook_print(teardown)} + +teststeps: +- + name: headers + variables: + a: 123 + request: + url: /headers + method: GET + setup_hooks: + - ${setup_hook_add_kwargs($request)} + - ${setup_hook_remove_kwargs($request)} + teardown_hooks: + - ${teardown_hook_sleep_N_secs($response, 1)} + validate: + - eq: ["status_code", 200] + - contained_by: [body.headers.Host, "${get_httpbin_server()}"] + +- + name: alter response + request: + url: /headers + method: GET + teardown_hooks: + - ${alter_response($response)} + validate: + - eq: ["status_code", 500] + - eq: [headers."Content-Type", "html/text"] + - eq: [body.headers.Host, "127.0.0.1:8888"] diff --git a/examples/httpbin/hooks_test.py b/examples/httpbin/hooks_test.py new file mode 100644 index 0000000..4a074ba --- /dev/null +++ b/examples/httpbin/hooks_test.py @@ -0,0 +1,35 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: hooks.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseHooks(HttpRunner): + + config = Config("basic test with httpbin").base_url("${get_httpbin_server()}") + + teststeps = [ + Step( + RunRequest("headers") + .with_variables(**{"a": 123}) + .setup_hook("${setup_hook_add_kwargs($request)}") + .setup_hook("${setup_hook_remove_kwargs($request)}") + .get("/headers") + .teardown_hook("${teardown_hook_sleep_N_secs($response, 1)}") + .validate() + .assert_equal("status_code", 200) + .assert_contained_by("body.headers.Host", "${get_httpbin_server()}") + ), + Step( + RunRequest("alter response") + .get("/headers") + .teardown_hook("${alter_response($response)}") + .validate() + .assert_equal("status_code", 500) + .assert_equal('headers."Content-Type"', "html/text") + .assert_equal("body.headers.Host", "127.0.0.1:8888") + ), + ] + + +if __name__ == "__main__": + TestCaseHooks().test_start() diff --git a/examples/httpbin/load_image.yml b/examples/httpbin/load_image.yml new file mode 100644 index 0000000..7a2ada6 --- /dev/null +++ b/examples/httpbin/load_image.yml @@ -0,0 +1,37 @@ +config: + name: load images + base_url: ${get_httpbin_server()} + +teststeps: +- + name: get png image + request: + url: /image/png + method: GET + validate: + - eq: ["status_code", 200] + +- + name: get jpeg image + request: + url: /image/jpeg + method: GET + validate: + - eq: ["status_code", 200] + +- + name: get webp image + request: + url: /image/webp + method: GET + validate: + - eq: ["status_code", 200] + +- + name: get svg image + request: + url: /image/svg + method: GET + validate: + - eq: ["status_code", 200] + diff --git a/examples/httpbin/load_image_test.py b/examples/httpbin/load_image_test.py new file mode 100644 index 0000000..324a2d5 --- /dev/null +++ b/examples/httpbin/load_image_test.py @@ -0,0 +1,39 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: load_image.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseLoadImage(HttpRunner): + + config = Config("load images").base_url("${get_httpbin_server()}") + + teststeps = [ + Step( + RunRequest("get png image") + .get("/image/png") + .validate() + .assert_equal("status_code", 200) + ), + Step( + RunRequest("get jpeg image") + .get("/image/jpeg") + .validate() + .assert_equal("status_code", 200) + ), + Step( + RunRequest("get webp image") + .get("/image/webp") + .validate() + .assert_equal("status_code", 200) + ), + Step( + RunRequest("get svg image") + .get("/image/svg") + .validate() + .assert_equal("status_code", 200) + ), + ] + + +if __name__ == "__main__": + TestCaseLoadImage().test_start() diff --git a/examples/httpbin/test.env b/examples/httpbin/test.env new file mode 100644 index 0000000..74d5d9e --- /dev/null +++ b/examples/httpbin/test.env @@ -0,0 +1,4 @@ +UserName=test +Password=654321 +PROJECT_KEY=AAABBBCCC +content_type=application/json; charset=UTF-8 \ No newline at end of file diff --git a/examples/httpbin/upload.yml b/examples/httpbin/upload.yml new file mode 100644 index 0000000..5eff4cb --- /dev/null +++ b/examples/httpbin/upload.yml @@ -0,0 +1,30 @@ +config: + name: test upload file with httpbin + base_url: ${get_httpbin_server()} + +teststeps: +- + name: upload file + variables: + file_path: "test.env" + m_encoder: ${multipart_encoder(file=$file_path)} + request: + url: /post + method: POST + headers: + Content-Type: ${multipart_content_type($m_encoder)} + data: $m_encoder + validate: + - eq: ["status_code", 200] + - startswith: ["body.files.file", "UserName=test"] + +- + name: upload file with keyword + request: + url: /post + method: POST + upload: + file: "test.env" + validate: + - eq: ["status_code", 200] + - startswith: ["body.files.file", "UserName=test"] diff --git a/examples/httpbin/upload_test.py b/examples/httpbin/upload_test.py new file mode 100644 index 0000000..860fe1b --- /dev/null +++ b/examples/httpbin/upload_test.py @@ -0,0 +1,38 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: upload.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseUpload(HttpRunner): + + config = Config("test upload file with httpbin").base_url("${get_httpbin_server()}") + + teststeps = [ + Step( + RunRequest("upload file") + .with_variables( + **{ + "file_path": "test.env", + "m_encoder": "${multipart_encoder(file=$file_path)}", + } + ) + .post("/post") + .with_headers(**{"Content-Type": "${multipart_content_type($m_encoder)}"}) + .with_data("$m_encoder") + .validate() + .assert_equal("status_code", 200) + .assert_startswith("body.files.file", "UserName=test") + ), + Step( + RunRequest("upload file with keyword") + .post("/post") + .upload(**{"file": "test.env"}) + .validate() + .assert_equal("status_code", 200) + .assert_startswith("body.files.file", "UserName=test") + ), + ] + + +if __name__ == "__main__": + TestCaseUpload().test_start() diff --git a/examples/httpbin/user_agent.csv b/examples/httpbin/user_agent.csv new file mode 100644 index 0000000..fa0c1df --- /dev/null +++ b/examples/httpbin/user_agent.csv @@ -0,0 +1,4 @@ +user_agent +iOS/10.1 +iOS/10.2 +iOS/10.3 diff --git a/examples/httpbin/validate.yml b/examples/httpbin/validate.yml new file mode 100644 index 0000000..3f16d3f --- /dev/null +++ b/examples/httpbin/validate.yml @@ -0,0 +1,35 @@ +config: + name: basic test with httpbin + base_url: ${get_httpbin_server()} + +teststeps: +- + name: validate response with json path + request: + url: /get + params: + a: 1 + b: 2 + method: GET + validate: + - eq: ["status_code", 200] + - eq: ["body.args.a", "1"] + - eq: ["body.args.b", "2"] + validate_script: + - "assert status_code == 200" + + +- + name: validate response with python script + request: + url: /get + params: + a: 1 + b: 2 + method: GET + validate: + - eq: ["status_code", 200] + validate_script: + - "assert status_code == 201" + - "a = response_json.get('args').get('a')" + - "assert a == '1'" diff --git a/examples/httpbin/validate_test.py b/examples/httpbin/validate_test.py new file mode 100644 index 0000000..3e4dfc9 --- /dev/null +++ b/examples/httpbin/validate_test.py @@ -0,0 +1,31 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: validate.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseValidate(HttpRunner): + + config = Config("basic test with httpbin").base_url("${get_httpbin_server()}") + + teststeps = [ + Step( + RunRequest("validate response with json path") + .get("/get") + .with_params(**{"a": 1, "b": 2}) + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.a", "1") + .assert_equal("body.args.b", "2") + ), + Step( + RunRequest("validate response with python script") + .get("/get") + .with_params(**{"a": 1, "b": 2}) + .validate() + .assert_equal("status_code", 200) + ), + ] + + +if __name__ == "__main__": + TestCaseValidate().test_start() diff --git a/examples/postman_echo/.debugtalk_gen.py b/examples/postman_echo/.debugtalk_gen.py new file mode 100644 index 0000000..f3e5887 --- /dev/null +++ b/examples/postman_echo/.debugtalk_gen.py @@ -0,0 +1,20 @@ +# NOTE: Generated By hrp v4.2.0, DO NOT EDIT! + +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from debugtalk import * + + +if __name__ == "__main__": + import funppy + funppy.register("get_httprunner_version", get_httprunner_version) + funppy.register("sum_two", sum_two) + funppy.register("get_testcase_config_variables", get_testcase_config_variables) + funppy.register("get_testsuite_config_variables", get_testsuite_config_variables) + funppy.register("get_app_version", get_app_version) + funppy.register("calculate_two_nums", calculate_two_nums) + funppy.register("fake_rand_count", fake_rand_count) + funppy.serve() diff --git a/examples/postman_echo/__init__.py b/examples/postman_echo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/postman_echo/conftest.py b/examples/postman_echo/conftest.py new file mode 100644 index 0000000..c894700 --- /dev/null +++ b/examples/postman_echo/conftest.py @@ -0,0 +1,65 @@ +# NOTICE: Generated By HttpRunner. +import json +import os +import time + +import pytest +from loguru import logger + +from httprunner.utils import get_platform, ExtendJSONEncoder + + +@pytest.fixture(scope="session", autouse=True) +def session_fixture(request): + """setup and teardown each task""" + logger.info("start running testcases ...") + + start_at = time.time() + + yield + + logger.info("task finished, generate task summary for --save-tests") + + summary = { + "success": True, + "stat": { + "testcases": {"total": 0, "success": 0, "fail": 0}, + "teststeps": {"total": 0, "failures": 0, "successes": 0}, + }, + "time": {"start_at": start_at, "duration": time.time() - start_at}, + "platform": get_platform(), + "details": [], + } + + for item in request.node.items: + testcase_summary = item.instance.get_summary() + summary["success"] &= testcase_summary.success + + summary["stat"]["testcases"]["total"] += 1 + summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_results) + if testcase_summary.success: + summary["stat"]["testcases"]["success"] += 1 + summary["stat"]["teststeps"]["successes"] += len( + testcase_summary.step_results + ) + else: + summary["stat"]["testcases"]["fail"] += 1 + summary["stat"]["teststeps"]["successes"] += ( + len(testcase_summary.step_results) - 1 + ) + summary["stat"]["teststeps"]["failures"] += 1 + + testcase_summary_json = testcase_summary.dict() + testcase_summary_json["records"] = testcase_summary_json.pop("step_results") + summary["details"].append(testcase_summary_json) + + summary_path = os.path.join( + os.getcwd(), "examples/postman_echo/logs/request_methods/hardcode.summary.json" + ) + summary_dir = os.path.dirname(summary_path) + os.makedirs(summary_dir, exist_ok=True) + + with open(summary_path, "w", encoding="utf-8") as f: + json.dump(summary, f, indent=4, ensure_ascii=False, cls=ExtendJSONEncoder) + + logger.info(f"generated task summary: {summary_path}") diff --git a/examples/postman_echo/cookie_manipulation/__init__.py b/examples/postman_echo/cookie_manipulation/__init__.py new file mode 100644 index 0000000..70cfba5 --- /dev/null +++ b/examples/postman_echo/cookie_manipulation/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! diff --git a/examples/postman_echo/cookie_manipulation/hardcode.yml b/examples/postman_echo/cookie_manipulation/hardcode.yml new file mode 100644 index 0000000..d3b0035 --- /dev/null +++ b/examples/postman_echo/cookie_manipulation/hardcode.yml @@ -0,0 +1,34 @@ +config: + name: "set & delete cookies." + base_url: "https://postman-echo.com" + verify: False + export: ["cookie_foo1"] + +teststeps: +- + name: set cookie foo1 & foo2 & foo3 + request: + method: GET + url: /cookies/set + params: + foo1: bar1 + foo2: bar2 + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + extract: + cookie_foo1: body.cookies.foo1 + validate: + - eq: ["status_code", 200] + - eq: ["body.cookies.foo1", "bar1"] + - eq: ["body.cookies.foo2", "bar2"] +- + name: delete cookie foo2 + request: + method: GET + url: /cookies/delete?foo2 + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + validate: + - eq: ["status_code", 200] + - eq: ["body.cookies.foo1", "bar1"] + - eq: ["body.cookies.foo2", null] diff --git a/examples/postman_echo/cookie_manipulation/hardcode_test.py b/examples/postman_echo/cookie_manipulation/hardcode_test.py new file mode 100644 index 0000000..9abb014 --- /dev/null +++ b/examples/postman_echo/cookie_manipulation/hardcode_test.py @@ -0,0 +1,41 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: cookie_manipulation/hardcode.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseHardcode(HttpRunner): + + config = ( + Config("set & delete cookies.") + .base_url("https://postman-echo.com") + .verify(False) + .export(*["cookie_foo1"]) + ) + + teststeps = [ + Step( + RunRequest("set cookie foo1 & foo2 & foo3") + .get("/cookies/set") + .with_params(**{"foo1": "bar1", "foo2": "bar2"}) + .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) + .extract() + .with_jmespath("body.cookies.foo1", "cookie_foo1") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.cookies.foo1", "bar1") + .assert_equal("body.cookies.foo2", "bar2") + ), + Step( + RunRequest("delete cookie foo2") + .get("/cookies/delete?foo2") + .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.cookies.foo1", "bar1") + .assert_equal("body.cookies.foo2", None) + ), + ] + + +if __name__ == "__main__": + TestCaseHardcode().test_start() diff --git a/examples/postman_echo/cookie_manipulation/set_delete_cookies.yml b/examples/postman_echo/cookie_manipulation/set_delete_cookies.yml new file mode 100644 index 0000000..f43116a --- /dev/null +++ b/examples/postman_echo/cookie_manipulation/set_delete_cookies.yml @@ -0,0 +1,41 @@ +config: + name: "set & delete cookies." + variables: + foo1: bar1 + foo2: bar2 + base_url: "https://postman-echo.com" + verify: False + export: ["cookie_foo1", "cookie_foo3"] + +teststeps: +- + name: set cookie foo1 & foo2 & foo3 + variables: + foo3: bar3 + request: + method: GET + url: /cookies/set + params: + foo1: bar111 + foo2: $foo2 + foo3: $foo3 + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + extract: + cookie_foo1: $.cookies.foo1 + cookie_foo3: $.cookies.foo3 + validate: + - eq: ["status_code", 200] + - ne: ["$.cookies.foo3", "$foo3"] +- + name: delete cookie foo2 + request: + method: GET + url: /cookies/delete?foo2 + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + validate: + - eq: ["status_code", 200] + - ne: ["$.cookies.foo1", "$foo1"] + - eq: ["$.cookies.foo1", "$cookie_foo1"] + - eq: ["$.cookies.foo3", "$cookie_foo3"] diff --git a/examples/postman_echo/cookie_manipulation/set_delete_cookies_test.py b/examples/postman_echo/cookie_manipulation/set_delete_cookies_test.py new file mode 100644 index 0000000..fd408d1 --- /dev/null +++ b/examples/postman_echo/cookie_manipulation/set_delete_cookies_test.py @@ -0,0 +1,44 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: cookie_manipulation/set_delete_cookies.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseSetDeleteCookies(HttpRunner): + + config = ( + Config("set & delete cookies.") + .variables(**{"foo1": "bar1", "foo2": "bar2"}) + .base_url("https://postman-echo.com") + .verify(False) + .export(*["cookie_foo1", "cookie_foo3"]) + ) + + teststeps = [ + Step( + RunRequest("set cookie foo1 & foo2 & foo3") + .with_variables(**{"foo3": "bar3"}) + .get("/cookies/set") + .with_params(**{"foo1": "bar111", "foo2": "$foo2", "foo3": "$foo3"}) + .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) + .extract() + .with_jmespath("$.cookies.foo1", "cookie_foo1") + .with_jmespath("$.cookies.foo3", "cookie_foo3") + .validate() + .assert_equal("status_code", 200) + .assert_not_equal("$.cookies.foo3", "$foo3") + ), + Step( + RunRequest("delete cookie foo2") + .get("/cookies/delete?foo2") + .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) + .validate() + .assert_equal("status_code", 200) + .assert_not_equal("$.cookies.foo1", "$foo1") + .assert_equal("$.cookies.foo1", "$cookie_foo1") + .assert_equal("$.cookies.foo3", "$cookie_foo3") + ), + ] + + +if __name__ == "__main__": + TestCaseSetDeleteCookies().test_start() diff --git a/examples/postman_echo/debugtalk.py b/examples/postman_echo/debugtalk.py new file mode 100644 index 0000000..f17806a --- /dev/null +++ b/examples/postman_echo/debugtalk.py @@ -0,0 +1,42 @@ +from httprunner import __version__ + + +def get_httprunner_version(): + return __version__ + + +def sum_two(m, n): + return m + n + + +def get_testcase_config_variables(): + return {"foo1": "testcase_config_bar1", "foo2": "testcase_config_bar2"} + + +def get_testsuite_config_variables(): + return {"foo1": "testsuite_config_bar1", "foo2": "testsuite_config_bar2"} + + +def get_app_version(): + return [3.1, 3.0] + + +def calculate_two_nums(a, b=1): + return [a + b, b - a] + + +def fake_rand_count(): + """ + return 1 at first call + return 2 at second call + """ + l = [] + + def func(): + l.append(1) + return len(l) + + return func + + +fake_randnum = fake_rand_count() diff --git a/examples/postman_echo/request_methods/__init__.py b/examples/postman_echo/request_methods/__init__.py new file mode 100644 index 0000000..70cfba5 --- /dev/null +++ b/examples/postman_echo/request_methods/__init__.py @@ -0,0 +1 @@ +# NOTICE: Generated By HttpRunner. DO NOT EDIT! diff --git a/examples/postman_echo/request_methods/account.csv b/examples/postman_echo/request_methods/account.csv new file mode 100644 index 0000000..67ce22c --- /dev/null +++ b/examples/postman_echo/request_methods/account.csv @@ -0,0 +1,4 @@ +username,password +test1,111111 +test2,222222 +test3,333333 \ No newline at end of file diff --git a/examples/postman_echo/request_methods/conftest.py b/examples/postman_echo/request_methods/conftest.py new file mode 100644 index 0000000..9d872f1 --- /dev/null +++ b/examples/postman_echo/request_methods/conftest.py @@ -0,0 +1,61 @@ +import uuid +from typing import List + +import pytest +from httprunner import Config, Step +from loguru import logger + + +@pytest.fixture(scope="session", autouse=True) +def session_fixture(request): + """setup and teardown each task""" + total_testcases_num = request.node.testscollected + testcases = [] + for item in request.node.items: + testcase = { + "name": item.cls.config.name, + "path": item.cls.config.path, + "node_id": item.nodeid, + } + testcases.append(testcase) + + logger.debug(f"collected {total_testcases_num} testcases: {testcases}") + + yield + + logger.debug("teardown task fixture") + + # teardown task + # TODO: upload task summary + + +@pytest.fixture(scope="function", autouse=True) +def testcase_fixture(request): + """setup and teardown each testcase""" + config: Config = request.cls.config + teststeps: List[Step] = request.cls.teststeps + + logger.debug(f"setup testcase fixture: {config.name} - {request.module.__name__}") + + def update_request_headers(steps, index): + for teststep in steps: + if teststep.request: + index += 1 + teststep.request.headers["X-Request-ID"] = f"{prefix}-{index}" + elif teststep.testcase and hasattr(teststep.testcase, "teststeps"): + update_request_headers(teststep.testcase.teststeps, index) + + # you can update testcase teststep like this + prefix = f"HRUN-{uuid.uuid4()}" + update_request_headers(teststeps, 0) + + yield + + logger.debug( + f"teardown testcase fixture: {config.name} - {request.module.__name__}" + ) + + summary = request.instance.get_summary() + logger.debug(f"testcase result summary: {summary}") + + # TODO: upload testcase summary diff --git a/examples/postman_echo/request_methods/hardcode.yml b/examples/postman_echo/request_methods/hardcode.yml new file mode 100644 index 0000000..2a2a7a2 --- /dev/null +++ b/examples/postman_echo/request_methods/hardcode.yml @@ -0,0 +1,55 @@ +config: + name: "request methods testcase in hardcode" + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: get with params + request: + method: GET + url: /get + params: + foo1: bar1 + foo2: bar2 + headers: + :authority: postman-echo.com + :method: POST + :path: /get + :schema: https + User-Agent: HttpRunner/3.0 + validate: + - eq: ["status_code", 200] +- + name: post raw text + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/3.0 + Content-Type: "text/plain" + data: "This is expected to be sent back as part of response body." + validate: + - eq: ["status_code", 200] +- + name: post form data + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/3.0 + Content-Type: "application/x-www-form-urlencoded" + data: "foo1=bar1&foo2=bar2" + validate: + - eq: ["status_code", 200] +- + name: put request + request: + method: PUT + url: /put + headers: + User-Agent: HttpRunner/3.0 + Content-Type: "text/plain" + data: "This is expected to be sent back as part of response body." + validate: + - eq: ["status_code", 200] \ No newline at end of file diff --git a/examples/postman_echo/request_methods/hardcode_test.py b/examples/postman_echo/request_methods/hardcode_test.py new file mode 100644 index 0000000..ced3aa6 --- /dev/null +++ b/examples/postman_echo/request_methods/hardcode_test.py @@ -0,0 +1,68 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: request_methods/hardcode.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseHardcode(HttpRunner): + + config = ( + Config("request methods testcase in hardcode") + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunRequest("get with params") + .get("/get") + .with_params(**{"foo1": "bar1", "foo2": "bar2"}) + .with_headers( + **{ + ":authority": "postman-echo.com", + ":method": "POST", + ":path": "/get", + ":schema": "https", + "User-Agent": "HttpRunner/3.0", + } + ) + .validate() + .assert_equal("status_code", 200) + ), + Step( + RunRequest("post raw text") + .post("/post") + .with_headers( + **{"User-Agent": "HttpRunner/3.0", "Content-Type": "text/plain"} + ) + .with_data("This is expected to be sent back as part of response body.") + .validate() + .assert_equal("status_code", 200) + ), + Step( + RunRequest("post form data") + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/3.0", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=bar1&foo2=bar2") + .validate() + .assert_equal("status_code", 200) + ), + Step( + RunRequest("put request") + .put("/put") + .with_headers( + **{"User-Agent": "HttpRunner/3.0", "Content-Type": "text/plain"} + ) + .with_data("This is expected to be sent back as part of response body.") + .validate() + .assert_equal("status_code", 200) + ), + ] + + +if __name__ == "__main__": + TestCaseHardcode().test_start() diff --git a/examples/postman_echo/request_methods/request_with_functions.yml b/examples/postman_echo/request_methods/request_with_functions.yml new file mode 100644 index 0000000..98007e7 --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_functions.yml @@ -0,0 +1,69 @@ +config: + name: "request methods testcase with functions" + variables: + foo1: config_bar1 + foo2: config_bar2 + expect_foo1: config_bar1 + expect_foo2: config_bar2 + base_url: "https://postman-echo.com" + verify: False + weight: 2 + export: ["foo3"] + +teststeps: +- + name: get with params + variables: + foo1: bar11 + foo2: bar21 + sum_v: "${sum_two(1, 2)}" + request: + method: GET + url: /get + params: + foo1: $foo1 + foo2: $foo2 + sum_v: $sum_v + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + extract: + foo3: "body.args.foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.args.foo1", "bar11"] + - eq: ["body.args.sum_v", "3"] + - eq: ["body.args.foo2", "bar21"] +- + name: post raw text + variables: + foo1: "bar12" + foo3: "bar32" + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + Content-Type: "text/plain" + data: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." + validate: + - eq: ["status_code", 200] + - eq: ["body.data", "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32."] + - type_match: ["body.json", None] + - type_match: ["body.json", NoneType] + - type_match: ["body.json", null] +- + name: post form data + variables: + foo2: bar23 + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + Content-Type: "application/x-www-form-urlencoded" + data: "foo1=$foo1&foo2=$foo2&foo3=$foo3" + validate: + - eq: ["status_code", 200, "response status code should be 200"] + - eq: ["body.form.foo1", "$expect_foo1"] + - eq: ["body.form.foo2", "bar23"] + - eq: ["body.form.foo3", "bar21"] diff --git a/examples/postman_echo/request_methods/request_with_functions_test.py b/examples/postman_echo/request_methods/request_with_functions_test.py new file mode 100644 index 0000000..24596d7 --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_functions_test.py @@ -0,0 +1,84 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: request_methods/request_with_functions.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseRequestWithFunctions(HttpRunner): + + config = ( + Config("request methods testcase with functions") + .variables( + **{ + "foo1": "config_bar1", + "foo2": "config_bar2", + "expect_foo1": "config_bar1", + "expect_foo2": "config_bar2", + } + ) + .base_url("https://postman-echo.com") + .verify(False) + .export(*["foo3"]) + ) + + teststeps = [ + Step( + RunRequest("get with params") + .with_variables( + **{"foo1": "bar11", "foo2": "bar21", "sum_v": "${sum_two(1, 2)}"} + ) + .get("/get") + .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}) + .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) + .extract() + .with_jmespath("body.args.foo2", "foo3") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.foo1", "bar11") + .assert_equal("body.args.sum_v", "3") + .assert_equal("body.args.foo2", "bar21") + ), + Step( + RunRequest("post raw text") + .with_variables(**{"foo1": "bar12", "foo3": "bar32"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "text/plain", + } + ) + .with_data( + "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." + ) + .validate() + .assert_equal("status_code", 200) + .assert_equal( + "body.data", + "This is expected to be sent back as part of response body: bar12-$expect_foo2-bar32.", + ) + .assert_type_match("body.json", "None") + .assert_type_match("body.json", "NoneType") + .assert_type_match("body.json", None) + ), + Step( + RunRequest("post form data") + .with_variables(**{"foo2": "bar23"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$foo2&foo3=$foo3") + .validate() + .assert_equal("status_code", 200, "response status code should be 200") + .assert_equal("body.form.foo1", "$expect_foo1") + .assert_equal("body.form.foo2", "bar23") + .assert_equal("body.form.foo3", "bar21") + ), + ] + + +if __name__ == "__main__": + TestCaseRequestWithFunctions().test_start() diff --git a/examples/postman_echo/request_methods/request_with_parameters.yml b/examples/postman_echo/request_methods/request_with_parameters.yml new file mode 100644 index 0000000..38e239c --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_parameters.yml @@ -0,0 +1,33 @@ +config: + name: "request methods testcase: validate with parameters" + parameters: + user_agent: ["iOS/10.1", "iOS/10.2"] + username-password: ${parameterize(request_methods/account.csv)} + app_version: ${get_app_version()} + variables: + app_version: f1 + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: get with params + variables: + foo1: $username + foo2: $password + sum_v: "${sum_two(1, $app_version)}" + request: + method: GET + url: /get + params: + foo1: $foo1 + foo2: $foo2 + sum_v: $sum_v + headers: + User-Agent: $user_agent,$app_version + extract: + session_foo2: "body.args.foo2" + validate: + - eq: ["status_code", 200] + - str_eq: ["body.args.sum_v", "${sum_two(1, $app_version)}"] +# - less_than: ["body.args.sum_v", "${sum_two(2, 2)}"] FIXME: TypeError: '<' not supported between instances of 'str' and 'int' diff --git a/examples/postman_echo/request_methods/request_with_parameters_test.py b/examples/postman_echo/request_methods/request_with_parameters_test.py new file mode 100644 index 0000000..1b2d3e0 --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_parameters_test.py @@ -0,0 +1,53 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: request_methods/request_with_parameters.yml +import pytest + +from httprunner import HttpRunner, Config, Step, RunRequest +from httprunner import Parameters + + +class TestCaseRequestWithParameters(HttpRunner): + @pytest.mark.parametrize( + "param", + Parameters( + { + "user_agent": ["iOS/10.1", "iOS/10.2"], + "username-password": "${parameterize(request_methods/account.csv)}", + "app_version": "${get_app_version()}", + } + ), + ) + def test_start(self, param): + super().test_start(param) + + config = ( + Config("request methods testcase: validate with parameters") + .variables(**{"app_version": "f1"}) + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunRequest("get with params") + .with_variables( + **{ + "foo1": "$username", + "foo2": "$password", + "sum_v": "${sum_two(1, $app_version)}", + } + ) + .get("/get") + .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}) + .with_headers(**{"User-Agent": "$user_agent,$app_version"}) + .extract() + .with_jmespath("body.args.foo2", "session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_string_equals("body.args.sum_v", "${sum_two(1, $app_version)}") + ), + ] + + +if __name__ == "__main__": + TestCaseRequestWithParameters().test_start() diff --git a/examples/postman_echo/request_methods/request_with_retry_test.py b/examples/postman_echo/request_methods/request_with_retry_test.py new file mode 100644 index 0000000..af74697 --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_retry_test.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" + + @Date : 2022/4/7 + @File : request_with_retry.py + @Author : duanchao.bill + @Desc : + +""" + +from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase + + +class TestCaseRetry(HttpRunner): + config = ( + Config("request methods testcase in hardcode") + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunRequest("run with retry") + .with_retry(retry_times=1, retry_interval=1) + .get("/get") + .with_params(**{"foo1": "${fake_randnum()}"}) + .with_headers(**{"User-Agent": "HttpRunner/3.0"}) + .validate() + .assert_equal("body.args.foo1", "2") + ) + ] diff --git a/examples/postman_echo/request_methods/request_with_testcase_reference.yml b/examples/postman_echo/request_methods/request_with_testcase_reference.yml new file mode 100644 index 0000000..abb51f3 --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_testcase_reference.yml @@ -0,0 +1,37 @@ +config: + name: "request methods testcase: reference testcase" + variables: + foo1: testsuite_config_bar1 + expect_foo1: testsuite_config_bar1 + expect_foo2: config_bar2 + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: request with functions + variables: + foo1: testcase_ref_bar1 + expect_foo1: testcase_ref_bar1 + setup_hooks: + - ${sleep(0.1)} + testcase: request_methods/request_with_functions.yml + teardown_hooks: + - ${sleep(0.2)} + export: + - foo3 +- + name: post form data + variables: + foo1: bar1 + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + Content-Type: "application/x-www-form-urlencoded" + data: "foo1=$foo1&foo2=$foo3" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "bar1"] + - eq: ["body.form.foo2", "bar21"] diff --git a/examples/postman_echo/request_methods/request_with_testcase_reference_test.py b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py new file mode 100644 index 0000000..d8b5a07 --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_testcase_reference_test.py @@ -0,0 +1,62 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: request_methods/request_with_testcase_reference.yml +from httprunner import HttpRunner, Config, Step, RunRequest +from httprunner import RunTestCase + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from request_methods.request_with_functions_test import ( + TestCaseRequestWithFunctions as RequestWithFunctions, +) + + +class TestCaseRequestWithTestcaseReference(HttpRunner): + + config = ( + Config("request methods testcase: reference testcase") + .variables( + **{ + "foo1": "testsuite_config_bar1", + "expect_foo1": "testsuite_config_bar1", + "expect_foo2": "config_bar2", + } + ) + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunTestCase("request with functions") + .with_variables( + **{"foo1": "testcase_ref_bar1", "expect_foo1": "testcase_ref_bar1"} + ) + .setup_hook("${sleep(0.1)}") + .call(RequestWithFunctions) + .teardown_hook("${sleep(0.2)}") + .export(*["foo3"]) + ), + Step( + RunRequest("post form data") + .with_variables(**{"foo1": "bar1"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/${get_httprunner_version()}", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$foo3") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "bar1") + .assert_equal("body.form.foo2", "bar21") + ), + ] + + +if __name__ == "__main__": + TestCaseRequestWithTestcaseReference().test_start() diff --git a/examples/postman_echo/request_methods/request_with_variables.yml b/examples/postman_echo/request_methods/request_with_variables.yml new file mode 100644 index 0000000..34c8477 --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_variables.yml @@ -0,0 +1,78 @@ +config: + name: "request methods testcase with variables" + variables: ${get_testcase_config_variables()} + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: get with params + variables: + foo1: bar11 + foo2: bar21 + request: + method: GET + url: /get + params: + foo1: $foo1 + foo2: $foo2 + headers: + User-Agent: HttpRunner/3.0 + extract: + foo3: "body.args.foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.args.foo1", "bar11"] + - eq: ["body.args.foo2", "bar21"] +- + name: post raw text + variables: + foo1: "bar12" + foo3: "bar32" + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/3.0 + Content-Type: "text/plain" + data: "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." + validate: + - eq: ["status_code", 200] + - eq: ["body.data", "This is expected to be sent back as part of response body: bar12-testcase_config_bar2-bar32."] +- + name: post form data + variables: + foo2: bar23 + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/3.0 + Content-Type: "application/x-www-form-urlencoded" + data: "foo1=$foo1&foo2=$foo2&foo3=$foo3" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "testcase_config_bar1"] + - eq: ["body.form.foo2", "bar23"] + - eq: ["body.form.foo3", "bar21"] + +- + name: post form data using json + variables: + foo2: bar23 + jsondata: + foo1: $foo1 + foo2: $foo2 + foo3: $foo3 + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/3.0 + Content-Type: "application/json" + json: $jsondata + validate: + - eq: ["status_code", 200] + - eq: ["body.data.foo1", "testcase_config_bar1"] + - eq: ["body.data.foo2", "bar23"] + - eq: ["body.data.foo3", "bar21"] diff --git a/examples/postman_echo/request_methods/request_with_variables_test.py b/examples/postman_echo/request_methods/request_with_variables_test.py new file mode 100644 index 0000000..ae6b9dd --- /dev/null +++ b/examples/postman_echo/request_methods/request_with_variables_test.py @@ -0,0 +1,86 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: request_methods/request_with_variables.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseRequestWithVariables(HttpRunner): + + config = ( + Config("request methods testcase with variables") + .variables(**{"foo1": "testcase_config_bar1", "foo2": "testcase_config_bar2"}) + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunRequest("get with params") + .with_variables(**{"foo1": "bar11", "foo2": "bar21"}) + .get("/get") + .with_params(**{"foo1": "$foo1", "foo2": "$foo2"}) + .with_headers(**{"User-Agent": "HttpRunner/3.0"}) + .extract() + .with_jmespath("body.args.foo2", "foo3") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.foo1", "bar11") + .assert_equal("body.args.foo2", "bar21") + ), + Step( + RunRequest("post raw text") + .with_variables(**{"foo1": "bar12", "foo3": "bar32"}) + .post("/post") + .with_headers( + **{"User-Agent": "HttpRunner/3.0", "Content-Type": "text/plain"} + ) + .with_data( + "This is expected to be sent back as part of response body: $foo1-$foo2-$foo3." + ) + .validate() + .assert_equal("status_code", 200) + .assert_equal( + "body.data", + "This is expected to be sent back as part of response body: bar12-testcase_config_bar2-bar32.", + ) + ), + Step( + RunRequest("post form data") + .with_variables(**{"foo2": "bar23"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/3.0", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$foo2&foo3=$foo3") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "testcase_config_bar1") + .assert_equal("body.form.foo2", "bar23") + .assert_equal("body.form.foo3", "bar21") + ), + Step( + RunRequest("post form data using json") + .with_variables( + **{ + "foo2": "bar23", + "jsondata": {"foo1": "$foo1", "foo2": "$foo2", "foo3": "$foo3"}, + } + ) + .post("/post") + .with_headers( + **{"User-Agent": "HttpRunner/3.0", "Content-Type": "application/json"} + ) + .with_json("$jsondata") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.data.foo1", "testcase_config_bar1") + .assert_equal("body.data.foo2", "bar23") + .assert_equal("body.data.foo3", "bar21") + ), + ] + + +if __name__ == "__main__": + TestCaseRequestWithVariables().test_start() diff --git a/examples/postman_echo/request_methods/validate_with_functions.yml b/examples/postman_echo/request_methods/validate_with_functions.yml new file mode 100644 index 0000000..608f061 --- /dev/null +++ b/examples/postman_echo/request_methods/validate_with_functions.yml @@ -0,0 +1,29 @@ +config: + name: "request methods testcase: validate with functions" + variables: + foo1: session_bar1 + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: get with params + variables: + foo1: bar1 + foo2: session_bar2 + sum_v: "${sum_two(1, 2)}" + request: + method: GET + url: /get + params: + foo1: $foo1 + foo2: $foo2 + sum_v: $sum_v + headers: + User-Agent: HttpRunner/${get_httprunner_version()} + extract: + session_foo2: "body.args.foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.args.sum_v", "3"] +# - less_than: ["body.args.sum_v", "${sum_two(2, 2)}"] FIXME: TypeError: '<' not supported between instances of 'str' and 'int' diff --git a/examples/postman_echo/request_methods/validate_with_functions_test.py b/examples/postman_echo/request_methods/validate_with_functions_test.py new file mode 100644 index 0000000..1ad2041 --- /dev/null +++ b/examples/postman_echo/request_methods/validate_with_functions_test.py @@ -0,0 +1,34 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: request_methods/validate_with_functions.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseValidateWithFunctions(HttpRunner): + + config = ( + Config("request methods testcase: validate with functions") + .variables(**{"foo1": "session_bar1"}) + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunRequest("get with params") + .with_variables( + **{"foo1": "bar1", "foo2": "session_bar2", "sum_v": "${sum_two(1, 2)}"} + ) + .get("/get") + .with_params(**{"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}) + .with_headers(**{"User-Agent": "HttpRunner/${get_httprunner_version()}"}) + .extract() + .with_jmespath("body.args.foo2", "session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.sum_v", "3") + ), + ] + + +if __name__ == "__main__": + TestCaseValidateWithFunctions().test_start() diff --git a/examples/postman_echo/request_methods/validate_with_variables.yml b/examples/postman_echo/request_methods/validate_with_variables.yml new file mode 100644 index 0000000..3044af9 --- /dev/null +++ b/examples/postman_echo/request_methods/validate_with_variables.yml @@ -0,0 +1,58 @@ +config: + name: "request methods testcase: validate with variables" + variables: + foo1: session_bar1 + base_url: "https://postman-echo.com" + verify: False + +teststeps: +- + name: get with params + variables: + foo1: bar1 + foo2: session_bar2 + request: + method: GET + url: /get + params: + foo1: $foo1 + foo2: $foo2 + headers: + User-Agent: HttpRunner/3.0 + extract: + session_foo2: "body.args.foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.args.foo1", "$foo1"] + - eq: ["body.args.foo2", "$foo2"] +- + name: post raw text + variables: + foo1: "hello world" + foo3: "$session_foo2" + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/3.0 + Content-Type: "text/plain" + data: "This is expected to be sent back as part of response body: $foo1-$foo3." + validate: + - eq: ["status_code", 200] + - eq: ["body.data", "This is expected to be sent back as part of response body: hello world-$foo3."] +- + name: post form data + variables: + foo1: bar1 + foo2: bar2 + request: + method: POST + url: /post + headers: + User-Agent: HttpRunner/3.0 + Content-Type: "application/x-www-form-urlencoded" + data: "foo1=$foo1&foo2=$foo2" + validate: + - eq: ["status_code", 200] + - eq: ["body.form.foo1", "$foo1"] + - eq: ["body.form.foo2", "$foo2"] diff --git a/examples/postman_echo/request_methods/validate_with_variables_test.py b/examples/postman_echo/request_methods/validate_with_variables_test.py new file mode 100644 index 0000000..1fb7270 --- /dev/null +++ b/examples/postman_echo/request_methods/validate_with_variables_test.py @@ -0,0 +1,66 @@ +# NOTE: Generated By HttpRunner v4.3.5 +# FROM: request_methods/validate_with_variables.yml +from httprunner import HttpRunner, Config, Step, RunRequest + + +class TestCaseValidateWithVariables(HttpRunner): + + config = ( + Config("request methods testcase: validate with variables") + .variables(**{"foo1": "session_bar1"}) + .base_url("https://postman-echo.com") + .verify(False) + ) + + teststeps = [ + Step( + RunRequest("get with params") + .with_variables(**{"foo1": "bar1", "foo2": "session_bar2"}) + .get("/get") + .with_params(**{"foo1": "$foo1", "foo2": "$foo2"}) + .with_headers(**{"User-Agent": "HttpRunner/3.0"}) + .extract() + .with_jmespath("body.args.foo2", "session_foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.args.foo1", "$foo1") + .assert_equal("body.args.foo2", "$foo2") + ), + Step( + RunRequest("post raw text") + .with_variables(**{"foo1": "hello world", "foo3": "$session_foo2"}) + .post("/post") + .with_headers( + **{"User-Agent": "HttpRunner/3.0", "Content-Type": "text/plain"} + ) + .with_data( + "This is expected to be sent back as part of response body: $foo1-$foo3." + ) + .validate() + .assert_equal("status_code", 200) + .assert_equal( + "body.data", + "This is expected to be sent back as part of response body: hello world-$foo3.", + ) + ), + Step( + RunRequest("post form data") + .with_variables(**{"foo1": "bar1", "foo2": "bar2"}) + .post("/post") + .with_headers( + **{ + "User-Agent": "HttpRunner/3.0", + "Content-Type": "application/x-www-form-urlencoded", + } + ) + .with_data("foo1=$foo1&foo2=$foo2") + .validate() + .assert_equal("status_code", 200) + .assert_equal("body.form.foo1", "$foo1") + .assert_equal("body.form.foo2", "$foo2") + ), + ] + + +if __name__ == "__main__": + TestCaseValidateWithVariables().test_start() diff --git a/examples/pytest.ini b/examples/pytest.ini new file mode 100644 index 0000000..0aad31f --- /dev/null +++ b/examples/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +addopts = -s +# https://docs.pytest.org/en/latest/how-to/output.html +junit_logging = all +junit_duration_report = total +log_cli = False diff --git a/examples/sql/test_sql_demo.py b/examples/sql/test_sql_demo.py new file mode 100644 index 0000000..b0ed6f5 --- /dev/null +++ b/examples/sql/test_sql_demo.py @@ -0,0 +1,36 @@ +import sys +from pathlib import Path + +from httprunner.database.engine import DBEngine + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from httprunner import HttpRunner, Config, Step, RunSqlRequest # noqa:E402 + + +class TestCaseDemoSqlite(HttpRunner): + config = Config("run sqlite demo") + + teststeps = [ + Step( + RunSqlRequest("执行一个sqlite demo") + .fetchmany("select* from student;", 5) + .extract() + .with_jmespath("[0].name", "name") + .validate() + .assert_equal( + "[0]", + { + "id": 1, + "name": "Jack", + "fullname": {"first_name": "Jack", "last_name": "Tomson"}, + }, + ) + .assert_equal("[0].fullname.first_name", "Jack") + ) + ] + + def test_start(self): + eg = DBEngine(db_uri="sqlite:///../data/sqlite.db") + self.with_db_engine(eg) + super().test_start() diff --git a/httprunner/README.md b/httprunner/README.md new file mode 100644 index 0000000..c6d241b --- /dev/null +++ b/httprunner/README.md @@ -0,0 +1,115 @@ +# 代码阅读指南(python 部分) + +## 核心数据结构 + +HttpRunner 以 `TestCase` 为核心,将任意测试场景抽象为有序步骤的集合。 + +```py +class TestCase(BaseModel): + config: TConfig + teststeps: List[TStep] +``` + +针对每种测试步骤,统一继承自 `IStep`,并要求必须至少实现如下 4 个方法;步骤内容统一在 `run` 方法中进行实现。 + +```py +class IStep(object): + + def name(self) -> str: + raise NotImplementedError + + def type(self) -> str: + raise NotImplementedError + + def struct(self) -> TStep: + raise NotImplementedError + + def run(self, runner) -> StepData: + # runner: HttpRunner + raise NotImplementedError +``` + +我们只需遵循 `IStep` 的接口定义,即可实现各种类型的测试步骤类型。当前 python 版本已支持的步骤类型包括: + +- [request](step_request.py):发起单次 HTTP 请求 +- [testcase](step_testcase.py):引用执行其它测试用例文件 + +基于该机制,我们可以扩展支持新的协议类型,例如 HTTP2/WebSocket/RPC 等;同时也可以支持新的测试类型,例如 UI 自动化。甚至我们还可以在一个测试用例中混合调用多种不同的 Step 类型,例如实现 HTTP/RPC/UI 混合场景。 + +## 用例编写 + +## 运行主流程 + +### 整体控制器 pytest + +不同于 golang 版本,python 版本的控制逻辑都基于 `pytest` 的用例发现和执行机制。 + +- 如果是运行 JSON/YAML 格式的用例,hrp 会将用例转换为 pytest 支持的用例格式 +- 如果是要自行编写 pytest 测试用例,需要遵循 HttpRunner 的格式要求 + +### pytest 用例格式要求 + +所有测试用例要求都继承自 `HttpRunner`,然后 + +结构如下所示: + +```py +class TestCaseRequestWithFunctions(HttpRunner): + + config = ( + Config("request methods testcase with functions") + ) + + teststeps = [ + Step( + RunRequest("get with params")... + ), + Step( + RunRequest("post raw text")... + ), + Step( + RunRequest("post form data")... + ), + ] +``` + +完整案例可参考: + +- [request_with_functions_test.py](../examples/postman_echo/request_methods/request_with_functions_test.py):用例中包含了 requests 的情况 +- [request_with_testcase_reference_test.py](../examples/postman_echo/request_methods/request_with_testcase_reference_test.py):用例中包含了引用其它测试用例的情况 + +### 用例执行器 SessionRunner + +测试用例的具体执行都由 `SessionRunner` 完成,每个 TestCase 对应一个实例,在该实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。 + +```py +class SessionRunner(object): + config: Config + teststeps: List[object] # list of Step + ... +``` + +重点关注一个方法: + +- test_start:该方法将被 pytest 发现,作为启动执行入口,依次执行所有测试步骤 + +```go +def test_start(self, param: Dict = None) -> "SessionRunner": + """main entrance, discovered by pytest""" + self.__start_at = time.time() + try: + # run step in sequential order + for step in self.teststeps: + self.__run_step(step) + finally: + logger.info(f"generate testcase log: {self.__log_path}") + + self.__duration = time.time() - self.__start_at +``` + +在主流程中,SessionRunner 并不需要关注 step 的具体类型,统一都是调用 `step.run(self)`,具体实现逻辑都在对应 step 的 `run` 方法中。 + +```py +def run(self, runner: HttpRunner) -> StepData: + return self.__step.run(runner) +``` diff --git a/httprunner/__init__.py b/httprunner/__init__.py new file mode 100644 index 0000000..58838e3 --- /dev/null +++ b/httprunner/__init__.py @@ -0,0 +1,38 @@ +__version__ = "v4.3.5" +__description__ = "One-stop solution for HTTP(S) testing." + + +from httprunner.config import Config +from httprunner.parser import parse_parameters as Parameters +from httprunner.runner import HttpRunner +from httprunner.step import Step +from httprunner.step_request import RunRequest +from httprunner.step_sql_request import ( + RunSqlRequest, + StepSqlRequestExtraction, + StepSqlRequestValidation, +) +from httprunner.step_testcase import RunTestCase +from httprunner.step_thrift_request import ( + RunThriftRequest, + StepThriftRequestExtraction, + StepThriftRequestValidation, +) + + +__all__ = [ + "__version__", + "__description__", + "HttpRunner", + "Config", + "Step", + "RunRequest", + "RunSqlRequest", + "StepSqlRequestValidation", + "StepSqlRequestExtraction", + "RunTestCase", + "Parameters", + "RunThriftRequest", + "StepThriftRequestValidation", + "StepThriftRequestExtraction", +] diff --git a/httprunner/__main__.py b/httprunner/__main__.py new file mode 100644 index 0000000..6cc9a14 --- /dev/null +++ b/httprunner/__main__.py @@ -0,0 +1,5 @@ +from httprunner.cli import main + + +if __name__ == "__main__": + main() diff --git a/httprunner/builtin/__init__.py b/httprunner/builtin/__init__.py new file mode 100644 index 0000000..0c7cf6d --- /dev/null +++ b/httprunner/builtin/__init__.py @@ -0,0 +1,2 @@ +from httprunner.builtin.comparators import * +from httprunner.builtin.functions import * diff --git a/httprunner/builtin/comparators.py b/httprunner/builtin/comparators.py new file mode 100644 index 0000000..58f9f3c --- /dev/null +++ b/httprunner/builtin/comparators.py @@ -0,0 +1,129 @@ +""" +Built-in validate comparators. +""" + +import re +from typing import Text, Any, Union + + +def equal(check_value: Any, expect_value: Any, message: Text = ""): + assert check_value == expect_value, message + + +def greater_than( + check_value: Union[int, float], expect_value: Union[int, float], message: Text = "" +): + assert check_value > expect_value, message + + +def less_than( + check_value: Union[int, float], expect_value: Union[int, float], message: Text = "" +): + assert check_value < expect_value, message + + +def greater_or_equals( + check_value: Union[int, float], expect_value: Union[int, float], message: Text = "" +): + assert check_value >= expect_value, message + + +def less_or_equals( + check_value: Union[int, float], expect_value: Union[int, float], message: Text = "" +): + assert check_value <= expect_value, message + + +def not_equal(check_value: Any, expect_value: Any, message: Text = ""): + assert check_value != expect_value, message + + +def string_equals(check_value: Text, expect_value: Any, message: Text = ""): + assert str(check_value) == str(expect_value), message + + +def length_equal(check_value: Text, expect_value: int, message: Text = ""): + assert isinstance(expect_value, int), "expect_value should be int type" + assert len(check_value) == expect_value, message + + +def length_greater_than( + check_value: Text, expect_value: Union[int, float], message: Text = "" +): + assert isinstance( + expect_value, (int, float) + ), "expect_value should be int/float type" + assert len(check_value) > expect_value, message + + +def length_greater_or_equals( + check_value: Text, expect_value: Union[int, float], message: Text = "" +): + assert isinstance( + expect_value, (int, float) + ), "expect_value should be int/float type" + assert len(check_value) >= expect_value, message + + +def length_less_than( + check_value: Text, expect_value: Union[int, float], message: Text = "" +): + assert isinstance( + expect_value, (int, float) + ), "expect_value should be int/float type" + assert len(check_value) < expect_value, message + + +def length_less_or_equals( + check_value: Text, expect_value: Union[int, float], message: Text = "" +): + assert isinstance( + expect_value, (int, float) + ), "expect_value should be int/float type" + assert len(check_value) <= expect_value, message + + +def contains(check_value: Any, expect_value: Any, message: Text = ""): + assert isinstance( + check_value, (list, tuple, dict, str, bytes) + ), "expect_value should be list/tuple/dict/str/bytes type" + assert expect_value in check_value, message + + +def contained_by(check_value: Any, expect_value: Any, message: Text = ""): + assert isinstance( + expect_value, (list, tuple, dict, str, bytes) + ), "expect_value should be list/tuple/dict/str/bytes type" + assert check_value in expect_value, message + + +def type_match(check_value: Any, expect_value: Any, message: Text = ""): + def get_type(name): + if isinstance(name, type): + return name + elif isinstance(name, str): + try: + return __builtins__[name] + except KeyError: + raise ValueError(name) + else: + raise ValueError(name) + + if expect_value in ["None", "NoneType", None]: + assert check_value is None, message + else: + assert type(check_value) == get_type(expect_value), message + + +def regex_match(check_value: Text, expect_value: Any, message: Text = ""): + assert isinstance(expect_value, str), "expect_value should be Text type" + assert isinstance(check_value, str), "check_value should be Text type" + assert re.match(expect_value, check_value), message + + +def startswith(check_value: Any, expect_value: Any, message: Text = ""): + assert str(check_value).startswith(str(expect_value)), message + + +def endswith(check_value: Text, expect_value: Any, message: Text = ""): + assert str(check_value).endswith(str(expect_value)), message diff --git a/httprunner/builtin/functions.py b/httprunner/builtin/functions.py new file mode 100644 index 0000000..00f964f --- /dev/null +++ b/httprunner/builtin/functions.py @@ -0,0 +1,35 @@ +""" +Built-in functions used in YAML/JSON testcases. +""" + +import datetime +import random +import string +import time + +from httprunner.exceptions import ParamsError + + +def gen_random_string(str_len): + """generate random string with specified length""" + return "".join( + random.choice(string.ascii_letters + string.digits) for _ in range(str_len) + ) + + +def get_timestamp(str_len=13): + """get timestamp string, length can only between 0 and 16""" + if isinstance(str_len, int) and 0 < str_len < 17: + return str(time.time()).replace(".", "")[:str_len] + + raise ParamsError("timestamp length can only between 0 and 16.") + + +def get_current_date(fmt="%Y-%m-%d"): + """get current date, default format is %Y-%m-%d""" + return datetime.datetime.now().strftime(fmt) + + +def sleep(n_secs): + """sleep n seconds""" + time.sleep(n_secs) diff --git a/httprunner/cli.py b/httprunner/cli.py new file mode 100644 index 0000000..427ee4b --- /dev/null +++ b/httprunner/cli.py @@ -0,0 +1,152 @@ +import argparse +import enum +import os +import sys + +import pytest +from loguru import logger + +from httprunner import __description__, __version__ +from httprunner.compat import ensure_cli_args +from httprunner.make import init_make_parser, main_make +from httprunner.utils import ga4_client, init_logger, init_sentry_sdk + +init_sentry_sdk() + + +def init_parser_run(subparsers): + sub_parser_run = subparsers.add_parser( + "run", help="Make HttpRunner testcases and run with pytest." + ) + return sub_parser_run + + +def main_run(extra_args) -> enum.IntEnum: + ga4_client.send_event("hrun") + # keep compatibility with v2 + extra_args = ensure_cli_args(extra_args) + + tests_path_list = [] + extra_args_new = [] + for item in extra_args: + if not os.path.exists(item): + # item is not file/folder path + extra_args_new.append(item) + else: + # item is file/folder path + tests_path_list.append(item) + + if len(tests_path_list) == 0: + # has not specified any testcase path + logger.error(f"No valid testcase path in cli arguments: {extra_args}") + sys.exit(1) + + testcase_path_list = main_make(tests_path_list) + if not testcase_path_list: + logger.error("No valid testcases found, exit 1.") + sys.exit(1) + + if "--tb=short" not in extra_args_new: + extra_args_new.append("--tb=short") + + extra_args_new.extend(testcase_path_list) + logger.info(f"start to run tests with pytest. HttpRunner version: {__version__}") + return pytest.main(extra_args_new) + + +def main(): + """API test: parse command line options and run commands.""" + parser = argparse.ArgumentParser(description=__description__) + parser.add_argument( + "-V", "--version", dest="version", action="store_true", help="show version" + ) + + subparsers = parser.add_subparsers(help="sub-command help") + init_parser_run(subparsers) + sub_parser_make = init_make_parser(subparsers) + + if len(sys.argv) == 1: + # httprunner + parser.print_help() + sys.exit(0) + elif len(sys.argv) == 2: + # print help for sub-commands + if sys.argv[1] in ["-V", "--version"]: + # httprunner -V + print(f"{__version__}") + elif sys.argv[1] in ["-h", "--help"]: + # httprunner -h + parser.print_help() + elif sys.argv[1] == "run": + # httprunner run + pytest.main(["-h"]) + elif sys.argv[1] == "make": + # httprunner make + sub_parser_make.print_help() + sys.exit(0) + elif ( + len(sys.argv) == 3 and sys.argv[1] == "run" and sys.argv[2] in ["-h", "--help"] + ): + # httprunner run -h + pytest.main(["-h"]) + sys.exit(0) + + extra_args = [] + if len(sys.argv) >= 2 and sys.argv[1] in ["run"]: + args, extra_args = parser.parse_known_args() + else: + args = parser.parse_args() + + if args.version: + print(f"{__version__}") + sys.exit(0) + + # set log level + try: + index = extra_args.index("--log-level") + if index < len(extra_args) - 1: + level = extra_args[index + 1] + else: + # not specify log level value + level = "INFO" # default + except ValueError: + level = "INFO" # default + + init_logger(level) + + if sys.argv[1] == "run": + sys.exit(main_run(extra_args)) + elif sys.argv[1] == "make": + main_make(args.testcase_path) + + +def main_hrun_alias(): + """command alias + hrun = httprunner run + """ + if len(sys.argv) == 2: + if sys.argv[1] in ["-V", "--version"]: + # hrun -V + sys.argv = ["httprunner", "-V"] + elif sys.argv[1] in ["-h", "--help"]: + pytest.main(["-h"]) + sys.exit(0) + else: + # hrun /path/to/testcase + sys.argv.insert(1, "run") + else: + sys.argv.insert(1, "run") + + main() + + +def main_make_alias(): + """command alias + hmake = httprunner make + """ + sys.argv.insert(1, "make") + main() + + +if __name__ == "__main__": + main() diff --git a/httprunner/cli_test.py b/httprunner/cli_test.py new file mode 100644 index 0000000..8a95ef0 --- /dev/null +++ b/httprunner/cli_test.py @@ -0,0 +1,62 @@ +import io +import os +import sys +import unittest + +import pytest + +from httprunner import loader +from httprunner.cli import main, main_run + + +class TestCli(unittest.TestCase): + def setUp(self): + self.captured_output = io.StringIO() + sys.stdout = self.captured_output + + def tearDown(self): + sys.stdout = sys.__stdout__ # Reset redirect. + + def test_show_version(self): + sys.argv = ["hrun", "-V"] + + with self.assertRaises(SystemExit) as cm: + main() + + self.assertEqual(cm.exception.code, 0) + + from httprunner import __version__ + + self.assertIn(__version__, self.captured_output.getvalue().strip()) + + def test_show_help(self): + sys.argv = ["hrun", "-h"] + + with self.assertRaises(SystemExit) as cm: + main() + + self.assertEqual(cm.exception.code, 0) + + from httprunner import __description__ + + self.assertIn(__description__, self.captured_output.getvalue().strip()) + + def test_debug_pytest(self): + 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) + + def test_run_testcase_with_abnormal_path(self): + loader.project_meta = None + exit_code = main_run(["examples/data/a-b.c/2 3.yml"]) + self.assertEqual(exit_code, 0) + self.assertTrue(os.path.exists("examples/data/a_b_c/__init__.py")) + self.assertTrue(os.path.exists("examples/data/debugtalk.py")) + self.assertTrue(os.path.exists("examples/data/a_b_c/T1_test.py")) + self.assertTrue(os.path.exists("examples/data/a_b_c/T2_3_test.py")) diff --git a/httprunner/client.py b/httprunner/client.py new file mode 100644 index 0000000..8e45425 --- /dev/null +++ b/httprunner/client.py @@ -0,0 +1,238 @@ +import json +import time + +import requests +import urllib3 +from loguru import logger +from requests import Request, Response +from requests.exceptions import ( + InvalidSchema, + InvalidURL, + MissingSchema, + RequestException, +) + +from httprunner.models import RequestData, ResponseData +from httprunner.models import SessionData, ReqRespData +from httprunner.utils import lower_dict_keys, omit_long_data + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +class ApiResponse(Response): + def raise_for_status(self): + if hasattr(self, "error") and self.error: + raise self.error + Response.raise_for_status(self) + + +def get_req_resp_record(resp_obj: Response) -> ReqRespData: + """get request and response info from Response() object.""" + + def log_print(req_or_resp, r_type): + msg = f"\n================== {r_type} details ==================\n" + for key, value in req_or_resp.dict().items(): + if isinstance(value, dict) or isinstance(value, list): + value = json.dumps(value, indent=4, ensure_ascii=False) + + msg += "{:<8} : {}\n".format(key, value) + logger.debug(msg) + + # record actual request info + request_headers = dict(resp_obj.request.headers) + request_cookies = resp_obj.request._cookies.get_dict() + + request_body = resp_obj.request.body + if request_body is not None: + try: + request_body = json.loads(request_body) + except json.JSONDecodeError: + # str: a=1&b=2 + pass + except UnicodeDecodeError: + # bytes/bytearray: request body in protobuf + pass + except TypeError: + # neither str nor bytes/bytearray, e.g. + pass + + request_content_type = lower_dict_keys(request_headers).get("content-type") + if request_content_type and "multipart/form-data" in request_content_type: + # upload file type + request_body = "upload file stream (OMITTED)" + + request_data = RequestData( + method=resp_obj.request.method, + url=resp_obj.request.url, + headers=request_headers, + cookies=request_cookies, + body=request_body, + ) + + # log request details in debug mode + log_print(request_data, "request") + + # record response info + resp_headers = dict(resp_obj.headers) + lower_resp_headers = lower_dict_keys(resp_headers) + content_type = lower_resp_headers.get("content-type", "") + + if "image" in content_type: + # response is image type, record bytes content only + response_body = resp_obj.content + else: + try: + # try to record json data + response_body = resp_obj.json() + except ValueError: + # only record at most 512 text charactors + resp_text = resp_obj.text + response_body = omit_long_data(resp_text) + + response_data = ResponseData( + status_code=resp_obj.status_code, + cookies=resp_obj.cookies or {}, + encoding=resp_obj.encoding, + headers=resp_headers, + content_type=content_type, + body=response_body, + ) + + # log response details in debug mode + log_print(response_data, "response") + + req_resp_data = ReqRespData(request=request_data, response=response_data) + return req_resp_data + + +class HttpSession(requests.Session): + """ + Class for performing HTTP requests and holding (session-) cookies between requests (in order + to be able to log in and out of websites). Each request is logged so that HttpRunner can + display statistics. + + This is a slightly extended version of `python-request `_'s + :py:class:`requests.Session` class and mostly this class works exactly the same. + """ + + def __init__(self): + super(HttpSession, self).__init__() + self.data = SessionData() + + def update_last_req_resp_record(self, resp_obj): + """ + update request and response info from Response() object. + """ + # TODO: fix + self.data.req_resps.pop() + self.data.req_resps.append(get_req_resp_record(resp_obj)) + + def request(self, method, url, name=None, **kwargs): + """ + Constructs and sends a :py:class:`requests.Request`. + Returns :py:class:`requests.Response` object. + + :param method: + method for the new :class:`Request` object. + :param url: + URL for the new :class:`Request` object. + :param name: (optional) + Placeholder, make compatible with Locust's HttpSession + :param params: (optional) + Dictionary or bytes to be sent in the query string for the :class:`Request`. + :param data: (optional) + Dictionary or bytes to send in the body of the :class:`Request`. + :param headers: (optional) + Dictionary of HTTP Headers to send with the :class:`Request`. + :param cookies: (optional) + Dict or CookieJar object to send with the :class:`Request`. + :param files: (optional) + Dictionary of ``'filename': file-like-objects`` for multipart encoding upload. + :param auth: (optional) + Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth. + :param timeout: (optional) + How long to wait for the server to send data before giving up, as a float, or \ + a (`connect timeout, read timeout `_) tuple. + :type timeout: float or tuple + :param allow_redirects: (optional) + Set to True by default. + :type allow_redirects: bool + :param proxies: (optional) + Dictionary mapping protocol to the URL of the proxy. + :param stream: (optional) + whether to immediately download the response content. Defaults to ``False``. + :param verify: (optional) + if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided. + :param cert: (optional) + if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. + """ + self.data = SessionData() + + # timeout default to 120 seconds + kwargs.setdefault("timeout", 120) + + # set stream to True, in order to get client/server IP/Port + kwargs["stream"] = True + + start_timestamp = time.time() + response = self._send_request_safe_mode(method, url, **kwargs) + response_time_ms = round((time.time() - start_timestamp) * 1000, 2) + + try: + client_ip, client_port = response.raw._connection.sock.getsockname() + self.data.address.client_ip = client_ip + self.data.address.client_port = client_port + logger.debug(f"client IP: {client_ip}, Port: {client_port}") + except Exception: + pass + + try: + server_ip, server_port = response.raw._connection.sock.getpeername() + self.data.address.server_ip = server_ip + self.data.address.server_port = server_port + logger.debug(f"server IP: {server_ip}, Port: {server_port}") + except Exception: + pass + + # get length of the response content + content_size = int(dict(response.headers).get("content-length") or 0) + + # record the consumed time + self.data.stat.response_time_ms = response_time_ms + self.data.stat.elapsed_ms = response.elapsed.microseconds / 1000.0 + self.data.stat.content_size = content_size + + # record request and response histories, include 30X redirection + response_list = response.history + [response] + self.data.req_resps = [ + get_req_resp_record(resp_obj) for resp_obj in response_list + ] + + try: + response.raise_for_status() + except RequestException as ex: + logger.error(f"{str(ex)}") + else: + logger.info( + f"status_code: {response.status_code}, " + f"response_time(ms): {response_time_ms} ms, " + f"response_length: {content_size} bytes" + ) + + return response + + def _send_request_safe_mode(self, method, url, **kwargs): + """ + Send a HTTP request, and catch any exception that might occur due to connection problems. + Safe mode has been removed from requests 1.x. + """ + try: + return requests.Session.request(self, method, url, **kwargs) + except (MissingSchema, InvalidSchema, InvalidURL): + raise + except RequestException as ex: + resp = ApiResponse() + resp.error = ex + resp.status_code = 0 # with this status_code, content returns None + resp.request = Request(method, url).prepare() + return resp diff --git a/httprunner/client_test.py b/httprunner/client_test.py new file mode 100644 index 0000000..466d090 --- /dev/null +++ b/httprunner/client_test.py @@ -0,0 +1,73 @@ +import unittest + +from httprunner.client import HttpSession +from httprunner.utils import HTTP_BIN_URL + + +class TestHttpSession(unittest.TestCase): + def setUp(self): + self.session = HttpSession() + + def test_request_http(self): + self.session.request("get", f"{HTTP_BIN_URL}/get") + address = self.session.data.address + self.assertGreater(len(address.server_ip), 0) + self.assertEqual(address.server_port, 80) + self.assertGreater(len(address.client_ip), 0) + self.assertGreater(address.client_port, 10000) + + def test_request_https(self): + self.session.request("get", "https://postman-echo.com/get") + address = self.session.data.address + self.assertGreater(len(address.server_ip), 0) + self.assertEqual(address.server_port, 443) + self.assertGreater(len(address.client_ip), 0) + self.assertGreater(address.client_port, 10000) + + def test_request_http_allow_redirects(self): + self.session.request( + "get", + f"{HTTP_BIN_URL}/redirect-to?url=https%3A%2F%2Fgithub.com", + allow_redirects=True, + ) + address = self.session.data.address + self.assertNotEqual(address.server_ip, "N/A") + self.assertEqual(address.server_port, 443) + self.assertNotEqual(address.server_ip, "N/A") + self.assertGreater(address.client_port, 10000) + + def test_request_https_allow_redirects(self): + self.session.request( + "get", + "https://postman-echo.com/redirect-to?url=https%3A%2F%2Fgithub.com", + allow_redirects=True, + ) + address = self.session.data.address + self.assertNotEqual(address.server_ip, "N/A") + self.assertEqual(address.server_port, 443) + self.assertNotEqual(address.server_ip, "N/A") + self.assertGreater(address.client_port, 10000) + + def test_request_http_not_allow_redirects(self): + self.session.request( + "get", + f"{HTTP_BIN_URL}/redirect-to?url=https%3A%2F%2Fgithub.com", + allow_redirects=False, + ) + address = self.session.data.address + self.assertEqual(address.server_ip, "N/A") + self.assertEqual(address.server_port, 0) + self.assertEqual(address.client_ip, "N/A") + self.assertEqual(address.client_port, 0) + + def test_request_https_not_allow_redirects(self): + self.session.request( + "get", + "https://postman-echo.com/redirect-to?url=https%3A%2F%2Fgithub.com", + allow_redirects=False, + ) + address = self.session.data.address + self.assertEqual(address.server_ip, "N/A") + self.assertEqual(address.server_port, 0) + self.assertEqual(address.client_ip, "N/A") + self.assertEqual(address.client_port, 0) diff --git a/httprunner/compat.py b/httprunner/compat.py new file mode 100644 index 0000000..755d600 --- /dev/null +++ b/httprunner/compat.py @@ -0,0 +1,385 @@ +""" +This module handles compatibility issues between testcase format v2, v3 and v4. +""" +import os +import sys +from typing import List, Dict, Text, Union, Any + +from loguru import logger + +from httprunner import exceptions +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 + + +def convert_variables( + raw_variables: Union[Dict, Text], test_path: Text +) -> Dict[Text, Any]: + if isinstance(raw_variables, Dict): + return raw_variables + + elif isinstance(raw_variables, Text): + # get variables by function, e.g. ${get_variables()} + project_meta = load_project_meta(test_path) + variables = parse_data(raw_variables, {}, project_meta.functions) + + return variables + + else: + raise exceptions.TestCaseFormatError( + f"Invalid variables format: {raw_variables}" + ) + + +def _convert_request(request: Dict) -> Dict: + if "body" in request: + content_type = "" + if "headers" in request and "Content-Type" in request["headers"]: + content_type = request["headers"]["Content-Type"] + if content_type.startswith("application/json"): + request["json"] = request.pop("body") + else: + request["data"] = request.pop("body") + return _sort_request_by_custom_order(request) + + +def _convert_jmespath(raw: Text) -> Text: + if not isinstance(raw, Text): + raise exceptions.TestCaseFormatError(f"Invalid jmespath extractor: {raw}") + + # content.xx/json.xx => body.xx + if raw.startswith("content"): + raw = f"body{raw[len('content'):]}" + elif raw.startswith("json"): + raw = f"body{raw[len('json'):]}" + + raw_list = raw.split(".") + for i, item in enumerate(raw_list): + item = item.strip('"') + if item.lower().startswith("content-") or item.lower() in ["user-agent"]: + # add quotes for some field in white list + # e.g. headers.Content-Type => headers."Content-Type" + raw_list[i] = f'"{item}"' + + return ".".join(raw_list) + + +def _convert_extractors(extractors: Union[List, Dict]) -> Dict: + """convert extract list(v2) to dict(v3) + + Args: + extractors: [{"varA": "content.varA"}, {"varB": "json.varB"}] + + Returns: + {"varA": "body.varA", "varB": "body.varB"} + + """ + v3_extractors: Dict = {} + + if isinstance(extractors, List): + # [{"varA": "content.varA"}, {"varB": "json.varB"}] + for extractor in extractors: + if not isinstance(extractor, Dict): + logger.error(f"Invalid extractor: {extractors}") + sys.exit(1) + for k, v in extractor.items(): + v3_extractors[k] = v + elif isinstance(extractors, Dict): + # {"varA": "body.varA", "varB": "body.varB"} + v3_extractors = extractors + else: + logger.error(f"Invalid extractor: {extractors}") + sys.exit(1) + + for k, v in v3_extractors.items(): + v3_extractors[k] = _convert_jmespath(v) + + return v3_extractors + + +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"]) + + elif len(v) == 1: + # format2: {'eq': ['status_code', 201]} + comparator = list(v.keys())[0] + v[comparator][0] = _convert_jmespath(v[comparator][0]) + + return validators + + +def _sort_request_by_custom_order(request: Dict) -> Dict: + custom_order = [ + "method", + "url", + "params", + "headers", + "cookies", + "data", + "json", + "files", + "timeout", + "allow_redirects", + "proxies", + "verify", + "stream", + "auth", + "cert", + ] + return sort_dict_by_custom_order(request, custom_order) + + +def _sort_step_by_custom_order(step: Dict) -> Dict: + custom_order = [ + "name", + "variables", + "request", + "testcase", + "setup_hooks", + "teardown_hooks", + "extract", + "validate", + "validate_script", + ] + return sort_dict_by_custom_order(step, custom_order) + + +def _ensure_step_attachment(step: Dict) -> Dict: + test_dict = { + "name": step["name"], + } + + if "request" in step: + test_dict["request"] = _convert_request(step["request"]) + + if "variables" in step: + test_dict["variables"] = step["variables"] + + if "setup_hooks" in step: + test_dict["setup_hooks"] = step["setup_hooks"] + + if "teardown_hooks" in step: + test_dict["teardown_hooks"] = step["teardown_hooks"] + + if "extract" in step: + test_dict["extract"] = _convert_extractors(step["extract"]) + + if "export" in step: + test_dict["export"] = step["export"] + + if "validate" in step: + if not isinstance(step["validate"], List): + raise exceptions.TestCaseFormatError( + f'Invalid teststep validate: {step["validate"]}' + ) + test_dict["validate"] = _convert_validators(step["validate"]) + + if "validate_script" in step: + test_dict["validate_script"] = step["validate_script"] + + return test_dict + + +def ensure_testcase_v4_api(api_content: Dict) -> Dict: + logger.info("convert api in v2/v3 to testcase format v4") + + teststep = { + "request": _convert_request(api_content["request"]), + } + teststep.update(_ensure_step_attachment(api_content)) + + teststep = _sort_step_by_custom_order(teststep) + + config = {"name": api_content["name"]} + extract_variable_names: List = list(teststep.get("extract", {}).keys()) + if extract_variable_names: + config["export"] = extract_variable_names + + return { + "config": config, + "teststeps": [teststep], + } + + +def ensure_testcase_v4(test_content: Dict) -> Dict: + logger.info("ensure compatibility with testcase format v2/v3") + + 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: + pass + elif "api" in step: + teststep["testcase"] = step.pop("api") + elif "testcase" in step: + teststep["testcase"] = step.pop("testcase") + else: + raise exceptions.TestCaseFormatError(f"Invalid teststep: {step}") + + teststep.update(_ensure_step_attachment(step)) + + teststep = _sort_step_by_custom_order(teststep) + v3_content["teststeps"].append(teststep) + + return v3_content + + +def ensure_cli_args(args: List) -> List: + """ensure compatibility with deprecated cli args in v2""" + # remove deprecated --failfast + if "--failfast" in args: + logger.warning("remove deprecated argument: --failfast") + args.pop(args.index("--failfast")) + + # convert --report-file to --html + if "--report-file" in args: + logger.warning("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( + "generate conftest.py keep compatibility with --save-tests in v2" + ) + args.pop(args.index("--save-tests")) + _generate_conftest_for_summary(args) + + return args + + +def _generate_conftest_for_summary(args: List): + + for arg in args: + if os.path.exists(arg): + test_path = arg + # FIXME: several test paths maybe specified + break + else: + logger.error(f"No valid test path specified! \nargs: {args}") + sys.exit(1) + + conftest_content = '''# NOTICE: Generated By HttpRunner. +import json +import os +import time + +import pytest +from loguru import logger + +from httprunner.utils import get_platform, ExtendJSONEncoder + + +@pytest.fixture(scope="session", autouse=True) +def session_fixture(request): + """setup and teardown each task""" + logger.info("start running testcases ...") + + start_at = time.time() + + yield + + logger.info("task finished, generate task summary for --save-tests") + + summary = { + "success": True, + "stat": { + "testcases": {"total": 0, "success": 0, "fail": 0}, + "teststeps": {"total": 0, "failures": 0, "successes": 0}, + }, + "time": {"start_at": start_at, "duration": time.time() - start_at}, + "platform": get_platform(), + "details": [], + } + + for item in request.node.items: + testcase_summary = item.instance.get_summary() + summary["success"] &= testcase_summary.success + + summary["stat"]["testcases"]["total"] += 1 + summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_results) + if testcase_summary.success: + summary["stat"]["testcases"]["success"] += 1 + summary["stat"]["teststeps"]["successes"] += len( + testcase_summary.step_results + ) + else: + summary["stat"]["testcases"]["fail"] += 1 + summary["stat"]["teststeps"]["successes"] += ( + len(testcase_summary.step_results) - 1 + ) + summary["stat"]["teststeps"]["failures"] += 1 + + testcase_summary_json = testcase_summary.dict() + testcase_summary_json["records"] = testcase_summary_json.pop("step_results") + summary["details"].append(testcase_summary_json) + + summary_path = r"{{SUMMARY_PATH_PLACEHOLDER}}" + summary_dir = os.path.dirname(summary_path) + os.makedirs(summary_dir, exist_ok=True) + + with open(summary_path, "w", encoding="utf-8") as f: + json.dump(summary, f, indent=4, ensure_ascii=False, cls=ExtendJSONEncoder) + + logger.info(f"generated task summary: {summary_path}") + +''' + + 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 = 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) + dump_file_name = "all.summary.json" + else: + file_relative_folder_path, test_file = os.path.split(test_path_relative_path) + file_foder_path = os.path.join(logs_dir_path, file_relative_folder_path) + test_file_name, _ = os.path.splitext(test_file) + dump_file_name = f"{test_file_name}.summary.json" + + summary_path = os.path.join(file_foder_path, dump_file_name) + conftest_content = conftest_content.replace( + "{{SUMMARY_PATH_PLACEHOLDER}}", summary_path + ) + + dir_path = os.path.dirname(conftest_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + with open(conftest_path, "w", encoding="utf-8") as f: + f.write(conftest_content) + + logger.info("generated conftest.py to generate summary.json") + + +def ensure_path_sep(path: Text) -> Text: + """ensure compatibility with different path separators of Linux and Windows""" + if "/" in path: + path = os.sep.join(path.split("/")) + + if "\\" in path: + path = os.sep.join(path.split("\\")) + + return path diff --git a/httprunner/compat_test.py b/httprunner/compat_test.py new file mode 100644 index 0000000..ef4a60d --- /dev/null +++ b/httprunner/compat_test.py @@ -0,0 +1,266 @@ +import os +import unittest + +from httprunner import compat, exceptions, loader +from httprunner.utils import HTTP_BIN_URL + + +class TestCompat(unittest.TestCase): + def setUp(self): + loader.project_meta = None + + def test_convert_variables(self): + raw_variables = {"var1": 1, "var2": "val2"} + self.assertEqual( + compat.convert_variables(raw_variables, "examples/data/a-b.c/1.yml"), + {"var1": 1, "var2": "val2"}, + ) + raw_variables = "${get_variables()}" + self.assertEqual( + compat.convert_variables(raw_variables, "examples/data/a-b.c/1.yml"), + {"foo1": "session_bar1"}, + ) + + with self.assertRaises(exceptions.TestCaseFormatError): + raw_variables = [{"var1": 1}, {"var2": "val2", "var3": 3}] + compat.convert_variables(raw_variables, "examples/data/a-b.c/1.yml") + with self.assertRaises(exceptions.TestCaseFormatError): + compat.convert_variables(None, "examples/data/a-b.c/1.yml") + + def test_convert_request(self): + request_with_json_body = { + "method": "POST", + "url": "https://postman-echo.com/post", + "headers": {"Content-Type": "application/json"}, + "body": {"k1": "v1", "k2": "v2"}, + } + self.assertEqual( + compat._convert_request(request_with_json_body), + { + "method": "POST", + "url": "https://postman-echo.com/post", + "headers": {"Content-Type": "application/json"}, + "json": {"k1": "v1", "k2": "v2"}, + }, + ) + + request_with_text_body = { + "method": "POST", + "url": "https://postman-echo.com/post", + "headers": {"Content-Type": "text/plain"}, + "body": "have a nice day", + } + self.assertEqual( + compat._convert_request(request_with_text_body), + { + "method": "POST", + "url": "https://postman-echo.com/post", + "headers": {"Content-Type": "text/plain"}, + "data": "have a nice day", + }, + ) + + 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("headers.Content-Type"), 'headers."Content-Type"' + ) + self.assertEqual( + compat._convert_jmespath("headers.User-Agent"), 'headers."User-Agent"' + ) + self.assertEqual( + compat._convert_jmespath('headers."Content-Type"'), 'headers."Content-Type"' + ) + self.assertEqual( + compat._convert_jmespath("body.users[-1]"), + "body.users[-1]", + ) + self.assertEqual( + compat._convert_jmespath("body.result.WorkNode_-1"), + "body.result.WorkNode_-1", + ) + + def test_convert_extractors(self): + self.assertEqual( + compat._convert_extractors( + [{"varA": "content.varA"}, {"varB": "json.varB"}] + ), + {"varA": "body.varA", "varB": "body.varB"}, + ) + self.assertEqual( + compat._convert_extractors([{"varA": "content[0].varA"}]), + {"varA": "body[0].varA"}, + ) + self.assertEqual( + compat._convert_extractors({"varA": "content[0].varA"}), + {"varA": "body[0].varA"}, + ) + + def test_convert_validators(self): + self.assertEqual( + 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]}]), + [{"eq": ["body.abc", 201]}], + ) + self.assertEqual( + compat._convert_validators([{"eq": ["content[0].name", 201]}]), + [{"eq": ["body[0].name", 201]}], + ) + + def test_ensure_testcase_v4_api(self): + api_content = { + "name": "get with params", + "request": { + "method": "GET", + "url": "/get", + "params": {"foo1": "bar1", "foo2": "bar2"}, + "headers": {"User-Agent": "HttpRunner/3.0"}, + }, + "extract": [{"varA": "content.varA"}, {"user_agent": "headers.User-Agent"}], + "validate": [{"eq": ["content.varB", 200]}, {"lt": ["json[0].varC", 0]}], + } + self.assertEqual( + compat.ensure_testcase_v4_api(api_content), + { + "config": { + "name": "get with params", + "export": ["varA", "user_agent"], + }, + "teststeps": [ + { + "name": "get with params", + "request": { + "method": "GET", + "url": "/get", + "params": {"foo1": "bar1", "foo2": "bar2"}, + "headers": {"User-Agent": "HttpRunner/3.0"}, + }, + "extract": { + "varA": "body.varA", + "user_agent": 'headers."User-Agent"', + }, + "validate": [ + {"eq": ["body.varB", 200]}, + {"lt": ["body[0].varC", 0]}, + ], + } + ], + }, + ) + + def test_ensure_testcase_v4(self): + testcase_content = { + "config": {"name": "xxx", "base_url": HTTP_BIN_URL}, + "teststeps": [ + { + "name": "get with params", + "request": { + "method": "GET", + "url": "/get", + "params": {"foo1": "bar1", "foo2": "bar2"}, + "headers": {"User-Agent": "HttpRunner/3.0"}, + }, + "extract": [ + {"varA": "content.varA"}, + {"user_agent": "headers.User-Agent"}, + ], + "validate": [ + {"eq": ["content.varB", 200]}, + {"lt": ["json[0].varC", 0]}, + ], + } + ], + } + self.assertEqual( + compat.ensure_testcase_v4(testcase_content), + { + "config": {"name": "xxx", "base_url": HTTP_BIN_URL}, + "teststeps": [ + { + "name": "get with params", + "request": { + "method": "GET", + "url": "/get", + "params": {"foo1": "bar1", "foo2": "bar2"}, + "headers": {"User-Agent": "HttpRunner/3.0"}, + }, + "extract": { + "varA": "body.varA", + "user_agent": 'headers."User-Agent"', + }, + "validate": [ + {"eq": ["body.varB", 200]}, + {"lt": ["body[0].varC", 0]}, + ], + } + ], + }, + ) + + def test_ensure_cli_args(self): + args1 = ["examples/postman_echo/request_methods/hardcode.yml", "--failfast"] + self.assertEqual( + compat.ensure_cli_args(args1), + ["examples/postman_echo/request_methods/hardcode.yml"], + ) + + args2 = ["examples/postman_echo/request_methods/hardcode.yml", "--save-tests"] + self.assertEqual( + compat.ensure_cli_args(args2), + ["examples/postman_echo/request_methods/hardcode.yml"], + ) + self.assertTrue(os.path.isfile("examples/postman_echo/conftest.py")) + + args3 = [ + "examples/postman_echo/request_methods/hardcode.yml", + "--report-file", + "report.html", + ] + self.assertEqual( + compat.ensure_cli_args(args3), + [ + "examples/postman_echo/request_methods/hardcode.yml", + "--html", + "report.html", + "--self-contained-html", + ], + ) + + args4 = [ + "examples/postman_echo/request_methods/hardcode.yml", + "--failfast", + "--save-tests", + "--report-file", + "report.html", + ] + self.assertEqual( + compat.ensure_cli_args(args4), + [ + "examples/postman_echo/request_methods/hardcode.yml", + "--html", + "report.html", + "--self-contained-html", + ], + ) + + def test_ensure_file_path(self): + self.assertEqual( + compat.ensure_path_sep("demo\\test.yml"), os.sep.join(["demo", "test.yml"]) + ) + self.assertEqual( + 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( + compat.ensure_path_sep("demo/test.yml"), os.sep.join(["demo", "test.yml"]) + ) + self.assertEqual( + compat.ensure_path_sep(os.path.join(os.getcwd(), "demo/test.yml")), + os.path.join(os.getcwd(), os.sep.join(["demo", "test.yml"])), + ) diff --git a/httprunner/config.py b/httprunner/config.py new file mode 100644 index 0000000..f68a5c6 --- /dev/null +++ b/httprunner/config.py @@ -0,0 +1,138 @@ +import copy +import inspect +from typing import Text + +from httprunner.models import TConfig, TConfigThrift, TConfigDB, ProtoType, VariablesMapping + + +class ConfigThrift(object): + def __init__(self, config: TConfig) -> None: + self.__config = config + self.__config.thrift = TConfigThrift() + + def psm(self, psm: Text) -> "ConfigThrift": + self.__config.thrift.psm = psm + return self + + def env(self, env: Text) -> "ConfigThrift": + self.__config.thrift.env = env + return self + + def cluster(self, cluster: Text) -> "ConfigThrift": + self.__config.thrift.cluster = cluster + return self + + def service_name(self, service_name: Text) -> "ConfigThrift": + self.__config.thrift.service_name = service_name + return self + + def method(self, method: Text) -> "ConfigThrift": + self.__config.thrift.method = method + return self + + def ip(self, service_name_: Text) -> "ConfigThrift": + self.__config.thrift.service_name = service_name_ + return self + + def port(self, port: int) -> "ConfigThrift": + self.__config.thrift.port = port + return self + + def timeout(self, timeout: int) -> "ConfigThrift": + self.__config.thrift.timeout = timeout + return self + + def proto_type(self, proto_type: ProtoType) -> "ConfigThrift": + self.__config.thrift.proto_type = proto_type + return self + + def trans_type(self, trans_type: ProtoType) -> "ConfigThrift": + self.__config.thrift.trans_type = trans_type + return self + + def struct(self) -> TConfig: + return self.__config + + +class ConfigDB(object): + def __init__(self, config: TConfig): + self.__config = config + self.__config.db = TConfigDB() + + def psm(self, psm): + self.__config.db.psm = psm + return self + + def user(self, user): + self.__config.db.user = user + return self + + def password(self, password): + self.__config.db.password = password + return self + + def ip(self, ip): + self.__config.db.ip = ip + return self + + def port(self, port: int): + self.__config.db.port = port + return self + + def database(self, database: Text): + self.__config.db.database = database + return self + + def struct(self) -> TConfig: + return self.__config + + +class Config(object): + def __init__(self, name: Text) -> None: + caller_frame = inspect.stack()[1] + self.__name: Text = name + self.__base_url: Text = "" + self.__variables: VariablesMapping = {} + self.__config = TConfig(name=name, path=caller_frame.filename) + + @property + def name(self) -> Text: + return self.__config.name + + @property + def path(self) -> Text: + return self.__config.path + + def variables(self, **variables) -> "Config": + self.__variables.update(variables) + return self + + def base_url(self, base_url: Text) -> "Config": + self.__base_url = base_url + return self + + def verify(self, verify: bool) -> "Config": + self.__config.verify = verify + return self + + def export(self, *export_var_name: Text) -> "Config": + self.__config.export.extend(export_var_name) + self.__config.export = list(set(self.__config.export)) + return self + + def struct(self) -> TConfig: + self.__init() + return self.__config + + def thrift(self) -> ConfigThrift: + self.__init() + return ConfigThrift(self.__config) + + def db(self) -> ConfigDB: + self.__init() + return ConfigDB(self.__config) + + def __init(self) -> None: + self.__config.name = self.__name + self.__config.base_url = self.__base_url + self.__config.variables = copy.copy(self.__variables) diff --git a/httprunner/database/engine.py b/httprunner/database/engine.py new file mode 100644 index 0000000..8a99ded --- /dev/null +++ b/httprunner/database/engine.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +import datetime +import json + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + + +class DBEngine(object): + def __init__(self, db_uri): + """ + db_uri = f'mysql+pymysql://{username}:{password}@{host}:{port}/{database}?charset=utf8mb4' + + """ + engine = create_engine(db_uri) + self.session = sessionmaker(bind=engine, autocommit=True)() + + @staticmethod + def value_decode(row: dict): + """ + Try to decode value of table + datetime.datetime-->string + datetime.date-->string + json str-->dict + :param row: + :return: + """ + for k, v in row.items(): + if isinstance(v, datetime.datetime): + row[k] = v.strftime("%Y-%m-%d %H:%M:%S") + elif isinstance(v, datetime.date): + row[k] = v.strftime("%Y-%m-%d") + elif isinstance(v, str): + try: + row[k] = json.loads(v) + except ValueError: + pass + + def _fetch(self, query, size=-1, commit=True): + query = query.strip() + result = self.session.execute(query) + if query.upper()[:6] == "SELECT": + if size < 0: + al = result.fetchall() + al = [dict(el) for el in al] + for el in al: + self.value_decode(el) + return al or None + elif size == 1: + on = dict(result.fetchone()) + self.value_decode(on) + return on or None + else: + mny = result.fetchmany(size) + mny = [dict(el) for el in mny] + for el in mny: + self.value_decode(el) + return mny or None + elif query.upper()[:6] in ("UPDATE", "DELETE", "INSERT"): + return {"rowcount": result.rowcount} + + def fetchone(self, query, commit=True): + return self._fetch(query, size=1, commit=commit) + + def fetchmany(self, query, size, commit=True): + return self._fetch(query=query, size=size, commit=commit) + + def fetchall(self, query, commit=True): + return self._fetch(query=query, size=-1, commit=commit) + + def insert(self, query, commit=True): + return self._fetch(query=query, commit=commit) + + def delete(self, query, commit=True): + return self._fetch(query=query, commit=commit) + + def update(self, query, commit=True): + return self._fetch(query=query, commit=commit) + + +if __name__ == "__main__": + # db = DBEngine("mysql+pymysql://xxxxx:xxxxx@10.0.0.1:3306/dbname?charset=utf8mb4") + db = DBEngine("sqlite:////Users/xxx/HttpRunner/examples/data/sqlite.db") + print(db.fetchmany(""" + select* from student""", 5)) + print(db.fetchmany("select* from student", 5)) diff --git a/httprunner/exceptions.py b/httprunner/exceptions.py new file mode 100644 index 0000000..e3969fe --- /dev/null +++ b/httprunner/exceptions.py @@ -0,0 +1,92 @@ +""" failure type exceptions + these exceptions will mark test as failure +""" + + +class MyBaseFailure(Exception): + pass + + +class ParseTestsFailure(MyBaseFailure): + pass + + +class ValidationFailure(MyBaseFailure): + pass + + +class ExtractFailure(MyBaseFailure): + pass + + +class SetupHooksFailure(MyBaseFailure): + pass + + +class TeardownHooksFailure(MyBaseFailure): + pass + + +""" error type exceptions + these exceptions will mark test as error +""" + + +class MyBaseError(Exception): + pass + + +class FileFormatError(MyBaseError): + pass + + +class TestCaseFormatError(FileFormatError): + pass + + +class TestSuiteFormatError(FileFormatError): + pass + + +class ParamsError(MyBaseError): + pass + + +class NotFoundError(MyBaseError): + pass + + +class FileNotFound(FileNotFoundError, NotFoundError): + pass + + +class FunctionNotFound(NotFoundError): + pass + + +class VariableNotFound(NotFoundError): + pass + + +class EnvNotFound(NotFoundError): + pass + + +class CSVNotFound(NotFoundError): + pass + + +class ApiNotFound(NotFoundError): + pass + + +class TestcaseNotFound(NotFoundError): + pass + + +class SummaryEmpty(MyBaseError): + """test result summary data is empty""" + + +class SqlMethodNotSupport(MyBaseError): + pass diff --git a/httprunner/ext/__init__.py b/httprunner/ext/__init__.py new file mode 100644 index 0000000..2ce5da7 --- /dev/null +++ b/httprunner/ext/__init__.py @@ -0,0 +1,2 @@ +# NOTICE: +# This file should not be deleted, or ImportError will be raised in Python 2.7 when importing extension diff --git a/httprunner/ext/uploader/__init__.py b/httprunner/ext/uploader/__init__.py new file mode 100644 index 0000000..d3a0bf0 --- /dev/null +++ b/httprunner/ext/uploader/__init__.py @@ -0,0 +1,178 @@ +""" upload test extension. + +If you want to use this extension, you should install the following dependencies first. + +- requests_toolbelt +- filetype + +Then you can write upload test script as below: + + - test: + name: upload file + request: + url: https://httpbin.org/upload + method: POST + headers: + Cookie: session=AAA-BBB-CCC + upload: + file: "data/file_to_upload" + field1: "value1" + field2: "value2" + validate: + - eq: ["status_code", 200] + +For compatibility, you can also write upload test script in old way: + + - test: + name: upload file + variables: + file: "data/file_to_upload" + field1: "value1" + field2: "value2" + m_encoder: ${multipart_encoder(file=$file, field1=$field1, field2=$field2)} + request: + url: https://httpbin.org/upload + method: POST + headers: + Content-Type: ${multipart_content_type($m_encoder)} + Cookie: session=AAA-BBB-CCC + data: $m_encoder + validate: + - eq: ["status_code", 200] + +""" + +import os +import sys +from typing import Text + +from httprunner.models import VariablesMapping, FunctionsMapping, TStep +from httprunner.parser import parse_data +from loguru import logger + +try: + import filetype + from requests_toolbelt import MultipartEncoder + + UPLOAD_READY = True +except ModuleNotFoundError: + UPLOAD_READY = False + + +def ensure_upload_ready(): + if UPLOAD_READY: + return + + msg = """ + uploader extension dependencies uninstalled, install first and try again. + install with pip: + $ pip install requests_toolbelt filetype + + or you can install httprunner with optional upload dependencies: + $ pip install "httprunner[upload]" + """ + logger.error(msg) + sys.exit(1) + + +def prepare_upload_step( + step: TStep, step_variables: VariablesMapping, functions: FunctionsMapping +): + """preprocess for upload test + replace `upload` info with MultipartEncoder + + Args: + step: teststep + { + "variables": {}, + "request": { + "url": "https://httpbin.org/upload", + "method": "POST", + "headers": { + "Cookie": "session=AAA-BBB-CCC" + }, + "upload": { + "file": "data/file_to_upload" + "md5": "123" + } + } + } + functions: functions mapping + + """ + if not step.request.upload: + return + + # parse upload info + step.request.upload = parse_data(step.request.upload, step_variables, functions) + + ensure_upload_ready() + params_list = [] + for key, value in step.request.upload.items(): + step_variables[key] = value + params_list.append(f"{key}=${key}") + + params_str = ", ".join(params_list) + step_variables["m_encoder"] = "${multipart_encoder(" + params_str + ")}" + + step.request.headers["Content-Type"] = "${multipart_content_type($m_encoder)}" + + step.request.data = "$m_encoder" + + +def multipart_encoder(**kwargs): + """initialize MultipartEncoder with uploading fields. + + Returns: + MultipartEncoder: initialized MultipartEncoder object + + """ + + def get_filetype(file_path): + file_type = filetype.guess(file_path) + if file_type: + return file_type.mime + else: + return "text/html" + + ensure_upload_ready() + fields_dict = {} + for key, value in kwargs.items(): + if os.path.isabs(value): + # value is absolute file path + _file_path = value + is_exists_file = os.path.isfile(value) + else: + # 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("") + + _file_path = os.path.join(project_meta.RootDir, value) + is_exists_file = os.path.isfile(_file_path) + + if is_exists_file: + # value is file path to upload + filename = os.path.basename(_file_path) + mime_type = get_filetype(_file_path) + # TODO: fix ResourceWarning for unclosed file + file_handler = open(_file_path, "rb") + fields_dict[key] = (filename, file_handler, mime_type) + else: + fields_dict[key] = value + + return MultipartEncoder(fields=fields_dict) + + +def multipart_content_type(m_encoder) -> Text: + """prepare Content-Type for request headers + + Args: + m_encoder: MultipartEncoder object + + Returns: + content type + + """ + ensure_upload_ready() + return m_encoder.content_type diff --git a/httprunner/loader.py b/httprunner/loader.py new file mode 100644 index 0000000..49dc849 --- /dev/null +++ b/httprunner/loader.py @@ -0,0 +1,432 @@ +import csv +import importlib +import json +import os +import sys +import types +from typing import Callable, Dict, List, Text, Tuple, Union + +import yaml +from loguru import logger +from pydantic import ValidationError + +from httprunner import builtin, exceptions, utils +from httprunner.models import ProjectMeta, TestCase + +project_meta: Union[ProjectMeta, None] = None + + +def _load_yaml_file(yaml_file: Text) -> Dict: + """load yaml file and check file content format""" + with open(yaml_file, mode="rb") as stream: + try: + yaml_content = yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as ex: + err_msg = f"YAMLError:\nfile: {yaml_file}\nerror: {ex}" + logger.error(err_msg) + raise exceptions.FileFormatError + + return yaml_content + + +def _load_json_file(json_file: Text) -> Dict: + """load json file and check file content format""" + with open(json_file, mode="rb") as data_file: + try: + json_content = json.load(data_file) + except json.JSONDecodeError as ex: + err_msg = f"JSONDecodeError:\nfile: {json_file}\nerror: {ex}" + raise exceptions.FileFormatError(err_msg) + + return json_content + + +def load_test_file(test_file: Text) -> Dict: + """load testcase/testsuite file content""" + if not os.path.isfile(test_file): + raise exceptions.FileNotFound(f"test file not exists: {test_file}") + + file_suffix = os.path.splitext(test_file)[1].lower() + if file_suffix == ".json": + test_file_content = _load_json_file(test_file) + elif file_suffix in [".yaml", ".yml"]: + test_file_content = _load_yaml_file(test_file) + else: + # '' or other suffix + raise exceptions.FileFormatError( + f"testcase/testsuite file should be YAML/JSON format, invalid format file: {test_file}" + ) + + return test_file_content + + +def load_testcase(testcase: Dict) -> TestCase: + try: + # validate with pydantic TestCase model + testcase_obj = TestCase.parse_obj(testcase) + except ValidationError as ex: + err_msg = f"TestCase ValidationError:\nerror: {ex}\ncontent: {testcase}" + raise exceptions.TestCaseFormatError(err_msg) + + return testcase_obj + + +def load_testcase_file(testcase_file: Text) -> TestCase: + """load testcase file and validate with pydantic model""" + testcase_content = load_test_file(testcase_file) + testcase_obj = load_testcase(testcase_content) + testcase_obj.config.path = testcase_file + return testcase_obj + + +def load_dot_env_file(dot_env_path: Text) -> Dict: + """load .env file. + + Args: + dot_env_path (str): .env file path + + Returns: + dict: environment variables mapping + + { + "UserName": "debugtalk", + "Password": "123456", + "PROJECT_KEY": "ABCDEFGH" + } + + Raises: + exceptions.FileFormatError: If .env file format is invalid. + + """ + if not os.path.isfile(dot_env_path): + return {} + + logger.info(f"Loading environment variables from {dot_env_path}") + env_variables_mapping = {} + + with open(dot_env_path, mode="rb") as fp: + for line in fp: + # maxsplit=1 + line = line.strip() + if not len(line) or line.startswith(b"#"): + continue + if b"=" in line: + variable, value = line.split(b"=", 1) + elif b":" in line: + variable, value = line.split(b":", 1) + else: + raise exceptions.FileFormatError(".env format error") + + env_variables_mapping[ + variable.strip().decode("utf-8") + ] = value.strip().decode("utf-8") + + utils.set_os_environ(env_variables_mapping) + return env_variables_mapping + + +def load_csv_file(csv_file: Text) -> List[Dict]: + """load csv file and check file content format + + Args: + csv_file (str): csv file path, csv file content is like below: + + Returns: + list: list of parameters, each parameter is in dict format + + Examples: + >>> cat csv_file + username,password + test1,111111 + test2,222222 + test3,333333 + + >>> load_csv_file(csv_file) + [ + {'username': 'test1', 'password': '111111'}, + {'username': 'test2', 'password': '222222'}, + {'username': 'test3', 'password': '333333'} + ] + + """ + if not os.path.isabs(csv_file): + global project_meta + if project_meta is None: + raise exceptions.MyBaseFailure("load_project_meta() has not been called!") + + # make compatible with Windows/Linux + csv_file = os.path.join(project_meta.RootDir, *csv_file.split("/")) + + if not os.path.isfile(csv_file): + # file path not exist + raise exceptions.CSVNotFound(csv_file) + + csv_content_list = [] + + with open(csv_file, encoding="utf-8") as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + csv_content_list.append(row) + + return csv_content_list + + +def load_folder_files(folder_path: Text, recursive: bool = True) -> List: + """load folder path, return all files endswith .yml/.yaml/.json/_test.py in list. + + Args: + folder_path (str): specified folder path to load + recursive (bool): load files recursively if True + + Returns: + list: files endswith yml/yaml/json + """ + if isinstance(folder_path, (list, set)): + files = [] + for path in set(folder_path): + files.extend(load_folder_files(path, recursive)) + + return files + + if not os.path.exists(folder_path): + return [] + + file_list = [] + + for dirpath, dirnames, filenames in os.walk(folder_path): + filenames_list = [] + + for filename in filenames: + if not filename.lower().endswith((".yml", ".yaml", ".json", "_test.py")): + continue + + filenames_list.append(filename) + + for filename in filenames_list: + file_path = os.path.join(dirpath, filename) + file_list.append(file_path) + + if not recursive: + break + + return file_list + + +def load_module_functions(module) -> Dict[Text, Callable]: + """load python module functions. + + Args: + module: python module + + Returns: + dict: functions mapping for specified python module + + { + "func1_name": func1, + "func2_name": func2 + } + + """ + module_functions = {} + + for name, item in vars(module).items(): + if isinstance(item, types.FunctionType): + module_functions[name] = item + + return module_functions + + +def load_builtin_functions() -> Dict[Text, Callable]: + """load builtin module functions""" + return load_module_functions(builtin) + + +def locate_file(start_path: Text, file_name: Text) -> Text: + """locate filename and return absolute file path. + searching will be recursive upward until system root dir. + + Args: + file_name (str): target locate file name + start_path (str): start locating path, maybe file path or directory path + + Returns: + str: located file path. None if file not found. + + Raises: + exceptions.FileNotFound: If failed to locate file. + + """ + if os.path.isfile(start_path): + start_dir_path = os.path.dirname(start_path) + elif os.path.isdir(start_path): + start_dir_path = start_path + else: + raise exceptions.FileNotFound(f"invalid path: {start_path}") + + 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 + # Windows, e.g. 'E:\\' + # Linux/Darwin, '/' + parent_dir = os.path.dirname(start_dir_path) + if parent_dir == start_dir_path: + raise exceptions.FileNotFound(f"{file_name} not found in {start_path}") + + # locate recursive upward + return locate_file(parent_dir, file_name) + + +def locate_debugtalk_py(start_path: Text) -> Text: + """locate debugtalk.py file + + Args: + start_path (str): start locating path, + maybe testcase file path or directory path + + Returns: + str: debugtalk.py file path, None if not found + + """ + try: + # locate debugtalk.py file. + debugtalk_path = locate_file(start_path, "debugtalk.py") + except exceptions.FileNotFound: + debugtalk_path = None + + return debugtalk_path + + +def locate_project_root_directory(test_path: Text) -> Tuple[Text, Text]: + """locate debugtalk.py path as project root directory + + Args: + test_path: specified testfile path + + Returns: + (str, str): debugtalk.py path, project_root_directory + + """ + + def prepare_path(path): + if not os.path.exists(path): + err_msg = f"path not exist: {path}" + logger.error(err_msg) + raise exceptions.FileNotFound(err_msg) + + if not os.path.isabs(path): + path = os.path.join(os.getcwd(), path) + + return path + + test_path = prepare_path(test_path) + + # locate debugtalk.py file + debugtalk_path = locate_debugtalk_py(test_path) + + if debugtalk_path: + # The folder contains debugtalk.py will be treated as project RootDir. + project_root_directory = os.path.dirname(debugtalk_path) + else: + # debugtalk.py not found, use os.getcwd() as project RootDir. + project_root_directory = os.getcwd() + + return debugtalk_path, project_root_directory + + +def load_debugtalk_functions() -> Dict[Text, Callable]: + """load project debugtalk.py module functions + debugtalk.py should be located in project root directory. + + Returns: + dict: debugtalk module functions mapping + { + "func1_name": func1, + "func2_name": func2 + } + + """ + # load debugtalk.py module + try: + imported_module = importlib.import_module("debugtalk") + except Exception as ex: + logger.error(f"error occurred in debugtalk.py: {ex}") + sys.exit(1) + + # reload to refresh previously loaded module + imported_module = importlib.reload(imported_module) + return load_module_functions(imported_module) + + +def load_project_meta(test_path: Text, reload: bool = False) -> ProjectMeta: + """load testcases, .env, debugtalk.py functions. + testcases folder is relative to project_root_directory + by default, project_meta will be loaded only once, unless set reload to true. + + Args: + test_path (str): test file/folder path, locate project RootDir from this path. + reload: reload project meta if set true, default to false + + Returns: + project loaded api/testcases definitions, + environments and debugtalk.py functions. + + """ + global project_meta + if project_meta and (not reload): + return project_meta + + project_meta = ProjectMeta() + + if not test_path: + return project_meta + + debugtalk_path, project_root_directory = locate_project_root_directory(test_path) + + # add project RootDir to sys.path + sys.path.insert(0, project_root_directory) + + # load .env file + # NOTICE: + # environment variable maybe loaded in debugtalk.py + # thus .env file should be loaded before loading debugtalk.py + dot_env_path = os.path.join(project_root_directory, ".env") + dot_env = load_dot_env_file(dot_env_path) + if dot_env: + project_meta.env = dot_env + project_meta.dot_env_path = dot_env_path + + if debugtalk_path: + # load debugtalk.py functions + debugtalk_functions = load_debugtalk_functions() + else: + debugtalk_functions = {} + + # locate project RootDir and load debugtalk.py functions + project_meta.RootDir = project_root_directory + project_meta.functions = debugtalk_functions + 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 :] diff --git a/httprunner/loader_test.py b/httprunner/loader_test.py new file mode 100644 index 0000000..7b09d87 --- /dev/null +++ b/httprunner/loader_test.py @@ -0,0 +1,127 @@ +import os +import unittest + +from httprunner import exceptions, loader + + +class TestLoader(unittest.TestCase): + def test_load_testcase_file(self): + path = "examples/postman_echo/request_methods/request_with_variables.yml" + testcase_obj = loader.load_testcase_file(path) + self.assertEqual( + testcase_obj.config.name, "request methods testcase with variables" + ) + self.assertEqual(len(testcase_obj.teststeps), 4) + + def test_load_json_file_file_format_error(self): + json_tmp_file = "tmp.json" + # create empty file + with open(json_tmp_file, "w") as f: + f.write("") + + with self.assertRaises(exceptions.FileFormatError): + loader._load_json_file(json_tmp_file) + + os.remove(json_tmp_file) + + # create empty json file + with open(json_tmp_file, "w") as f: + f.write("{}") + + loader._load_json_file(json_tmp_file) + os.remove(json_tmp_file) + + # create invalid format json file + with open(json_tmp_file, "w") as f: + f.write("abc") + + with self.assertRaises(exceptions.FileFormatError): + loader._load_json_file(json_tmp_file) + + os.remove(json_tmp_file) + + def test_load_testcases_bad_filepath(self): + testcase_file_path = os.path.join(os.getcwd(), "examples/data/demo") + with self.assertRaises(exceptions.FileNotFound): + loader.load_testcase_file(testcase_file_path) + + def test_load_csv_file_one_parameter(self): + csv_file_path = os.path.join(os.getcwd(), "examples/httpbin/user_agent.csv") + csv_content = loader.load_csv_file(csv_file_path) + self.assertEqual( + csv_content, + [ + {"user_agent": "iOS/10.1"}, + {"user_agent": "iOS/10.2"}, + {"user_agent": "iOS/10.3"}, + ], + ) + + def test_load_csv_file_multiple_parameters(self): + csv_file_path = os.path.join(os.getcwd(), "examples/httpbin/account.csv") + csv_content = loader.load_csv_file(csv_file_path) + self.assertEqual( + csv_content, + [ + {"username": "test1", "password": "111111"}, + {"username": "test2", "password": "222222"}, + {"username": "test3", "password": "333333"}, + ], + ) + + def test_load_folder_files(self): + folder = os.path.join(os.getcwd(), "examples") + file1 = os.path.join(os.getcwd(), "examples", "test_utils.py") + file2 = os.path.join(os.getcwd(), "examples", "httpbin", "hooks.yml") + + files = loader.load_folder_files(folder, recursive=False) + self.assertEqual(files, []) + + files = loader.load_folder_files(folder) + self.assertIn(file2, files) + self.assertNotIn(file1, files) + + files = loader.load_folder_files("not_existed_foulder", recursive=False) + self.assertEqual([], files) + + files = loader.load_folder_files(file2, recursive=False) + self.assertEqual([], files) + + def test_load_custom_dot_env_file(self): + dot_env_path = os.path.join(os.getcwd(), "examples", "httpbin", "test.env") + env_variables_mapping = loader.load_dot_env_file(dot_env_path) + self.assertIn("PROJECT_KEY", env_variables_mapping) + self.assertEqual(env_variables_mapping["UserName"], "test") + self.assertEqual( + env_variables_mapping["content_type"], "application/json; charset=UTF-8" + ) + + def test_load_env_path_not_exist(self): + dot_env_path = os.path.join( + os.getcwd(), + "tests", + "data", + ) + env_variables_mapping = loader.load_dot_env_file(dot_env_path) + self.assertEqual(env_variables_mapping, {}) + + def test_locate_file(self): + with self.assertRaises(exceptions.FileNotFound): + loader.locate_file(os.getcwd(), "debugtalk.py") + + with self.assertRaises(exceptions.FileNotFound): + loader.locate_file("", "debugtalk.py") + + start_path = os.path.join(os.getcwd(), "examples", "httpbin") + self.assertEqual( + loader.locate_file(start_path, "debugtalk.py"), + os.path.join(os.getcwd(), "examples", "httpbin", "debugtalk.py"), + ) + self.assertEqual( + loader.locate_file("examples/httpbin/", "debugtalk.py"), + os.path.join(os.getcwd(), "examples", "httpbin", "debugtalk.py"), + ) + self.assertEqual( + loader.locate_file("examples/httpbin/", "debugtalk.py"), + os.path.join(os.getcwd(), "examples", "httpbin", "debugtalk.py"), + ) diff --git a/httprunner/make.py b/httprunner/make.py new file mode 100644 index 0000000..d2b38ed --- /dev/null +++ b/httprunner/make.py @@ -0,0 +1,574 @@ +import os +import string +import subprocess +import sys +from typing import Dict, List, Set, Text, Tuple + +import jinja2 +from loguru import logger + +from httprunner import __version__, exceptions +from httprunner.compat import ( + convert_variables, + ensure_path_sep, + ensure_testcase_v4, + ensure_testcase_v4_api, +) +from httprunner.loader import ( + convert_relative_project_root_dir, + load_folder_files, + load_project_meta, + load_test_file, + load_testcase, +) +from httprunner.response import uniform_validator +from httprunner.utils import ga4_client, is_support_multiprocessing + +""" cache converted pytest files, avoid duplicate making +""" +pytest_files_made_cache_mapping: Dict[Text, Text] = {} + +""" save generated pytest files to run, except referenced testcase +""" +pytest_files_run_set: Set = set() + +__TEMPLATE__ = jinja2.Template( + """# NOTE: Generated By HttpRunner {{ version }} +# FROM: {{ testcase_path }} + +{%- if parameters or skip %} +import pytest +{% endif %} +from httprunner import HttpRunner, Config, Step, RunRequest + +{%- if parameters %} +from httprunner import Parameters +{%- endif %} + +{%- if reference_testcase %} +from httprunner import RunTestCase +{%- endif %} + +{%- for import_str in imports_list %} +{{ import_str }} +{%- endfor %} + +class {{ class_name }}(HttpRunner): + + {% if parameters and skip %} + @pytest.mark.parametrize("param", Parameters({{ parameters }})) + @pytest.mark.skip(reason={{ skip }}) + def test_start(self, param): + super().test_start(param) + + {% elif parameters %} + @pytest.mark.parametrize("param", Parameters({{ parameters }})) + def test_start(self, param): + super().test_start(param) + + {% elif skip %} + @pytest.mark.skip(reason={{ skip }}) + def test_start(self): + super().test_start() + {% endif %} + + config = {{ config_chain_style }} + + teststeps = [ + {% for step_chain_style in teststeps_chain_style %} + {{ step_chain_style }}, + {% endfor %} + ] + +if __name__ == "__main__": + {{ class_name }}().test_start() + +""" +) + + +def __ensure_absolute(path: Text) -> Text: + if path.startswith("./"): + # Linux/Darwin, hrun ./test.yml + path = path[2:] + elif path.startswith(".\\"): + # Windows, hrun .\\test.yml + path = path[3:] + + path = ensure_path_sep(path) + project_meta = load_project_meta(path) + + if os.path.isabs(path): + absolute_path = path + else: + absolute_path = os.path.join(project_meta.RootDir, path) + + if not os.path.isfile(absolute_path): + logger.error(f"Invalid testcase file path: {absolute_path}") + sys.exit(1) + + return absolute_path + + +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: + file_abs_path: absolute file path + + Returns: + ensured valid absolute file 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): + """ensure pytest files are in python module, generate __init__.py on demand""" + init_file = os.path.join(os.path.dirname(path), "__init__.py") + if os.path.isfile(init_file): + return + + with open(init_file, "w", encoding="utf-8") as f: + f.write("# NOTICE: Generated By HttpRunner. DO NOT EDIT!\n") + + +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_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_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_abs_path, name_in_title_case + + +def format_pytest_with_black(*python_paths: Text): + logger.info("format pytest cases with black ...") + try: + if is_support_multiprocessing() or len(python_paths) <= 1: + subprocess.run(["black", *python_paths]) + else: + logger.warning( + "this system does not support multiprocessing well, format files one by one ..." + ) + [subprocess.run(["black", path]) for path in python_paths] + except subprocess.CalledProcessError as ex: + logger.error(ex) + sys.exit(1) + except OSError: + err_msg = """ +missing dependency tool: black +install black manually and try again: +$ pip install black +""" + logger.error(err_msg) + sys.exit(1) + + +def make_config_chain_style(config: Dict) -> Text: + config_chain_style = f'Config("{config["name"]}")' + + if config["variables"]: + variables = config["variables"] + config_chain_style += f".variables(**{variables})" + + if "base_url" in config: + config_chain_style += f'.base_url("{config["base_url"]}")' + + if "verify" in config: + config_chain_style += f'.verify({config["verify"]})' + + if "export" in config: + config_chain_style += f'.export(*{config["export"]})' + + return config_chain_style + + +def make_config_skip(config: Dict) -> Text: + if "skip" in config: + if config["skip"]: + config_chain_style = config["skip"] + else: + config_chain_style = '"skip unconditionally"' + return config_chain_style + + +def make_request_chain_style(request: Dict) -> Text: + method = request["method"].lower() + url = request["url"] + request_chain_style = f'.{method}("{url}")' + + if "params" in request: + params = request["params"] + request_chain_style += f".with_params(**{params})" + + if "headers" in request: + headers = request["headers"] + request_chain_style += f".with_headers(**{headers})" + + if "cookies" in request: + cookies = request["cookies"] + request_chain_style += f".with_cookies(**{cookies})" + + if "data" in request: + data = request["data"] + if isinstance(data, Text): + data = f'"{data}"' + request_chain_style += f".with_data({data})" + + if "json" in request: + req_json = request["json"] + if isinstance(req_json, Text): + req_json = f'"{req_json}"' + request_chain_style += f".with_json({req_json})" + + if "timeout" in request: + timeout = request["timeout"] + request_chain_style += f".set_timeout({timeout})" + + if "verify" in request: + verify = request["verify"] + request_chain_style += f".set_verify({verify})" + + if "allow_redirects" in request: + allow_redirects = request["allow_redirects"] + request_chain_style += f".set_allow_redirects({allow_redirects})" + + if "upload" in request: + upload = request["upload"] + request_chain_style += f".upload(**{upload})" + + return request_chain_style + + +def make_teststep_chain_style(teststep: Dict) -> Text: + if teststep.get("request"): + step_info = f'RunRequest("{teststep["name"]}")' + elif teststep.get("testcase"): + step_info = f'RunTestCase("{teststep["name"]}")' + else: + raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}") + + if "variables" in teststep: + variables = teststep["variables"] + step_info += f".with_variables(**{variables})" + + if "setup_hooks" in teststep: + setup_hooks = teststep["setup_hooks"] + for hook in setup_hooks: + if isinstance(hook, Text): + step_info += f'.setup_hook("{hook}")' + elif isinstance(hook, Dict) and len(hook) == 1: + assign_var_name, hook_content = list(hook.items())[0] + step_info += f'.setup_hook("{hook_content}", "{assign_var_name}")' + else: + raise exceptions.TestCaseFormatError(f"Invalid setup hook: {hook}") + + if teststep.get("request"): + step_info += make_request_chain_style(teststep["request"]) + elif teststep.get("testcase"): + testcase = teststep["testcase"] + call_ref_testcase = f".call({testcase})" + step_info += call_ref_testcase + + if "teardown_hooks" in teststep: + teardown_hooks = teststep["teardown_hooks"] + for hook in teardown_hooks: + if isinstance(hook, Text): + step_info += f'.teardown_hook("{hook}")' + elif isinstance(hook, Dict) and len(hook) == 1: + assign_var_name, hook_content = list(hook.items())[0] + step_info += f'.teardown_hook("{hook_content}", "{assign_var_name}")' + else: + raise exceptions.TestCaseFormatError(f"Invalid teardown hook: {hook}") + + if "extract" in teststep: + # request step + step_info += ".extract()" + for extract_name, extract_path in teststep["extract"].items(): + step_info += f""".with_jmespath('{extract_path}', '{extract_name}')""" + + if "export" in teststep: + # reference testcase step + export: List[Text] = teststep["export"] + step_info += f".export(*{export})" + + if "validate" in teststep: + step_info += ".validate()" + + for v in teststep["validate"]: + validator = uniform_validator(v) + assert_method = validator["assert"] + check = validator["check"] + if '"' in check: + # e.g. body."user-agent" => 'body."user-agent"' + check = f"'{check}'" + else: + check = f'"{check}"' + expect = validator["expect"] + if isinstance(expect, Text): + expect = f'"{expect}"' + + message = validator["message"] + if message: + step_info += f".assert_{assert_method}({check}, {expect}, '{message}')" + else: + step_info += f".assert_{assert_method}({check}, {expect})" + + return f"Step({step_info})" + + +def make_testcase(testcase: Dict, dir_path: Text = None) -> Text: + """convert valid testcase dict to pytest file path""" + # ensure compatibility with testcase format v2/v3 + testcase = ensure_testcase_v4(testcase) + + # validate testcase format + load_testcase(testcase) + + testcase_abs_path = __ensure_absolute(testcase["config"]["path"]) + logger.info(f"start to make testcase: {testcase_abs_path}") + + testcase_python_abs_path, testcase_cls_name = convert_testcase_path( + testcase_abs_path + ) + if dir_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_abs_path in pytest_files_made_cache_mapping: + return testcase_python_abs_path + + config = testcase["config"] + config["path"] = convert_relative_project_root_dir(testcase_python_abs_path) + config["variables"] = convert_variables( + config.get("variables", {}), testcase_abs_path + ) + + # prepare reference testcase + imports_list = [] + teststeps = testcase["teststeps"] + for teststep in teststeps: + if not teststep.get("testcase"): + continue + + # make ref testcase pytest file + ref_testcase_path = __ensure_absolute(teststep["testcase"]) + test_content = load_test_file(ref_testcase_path) + + if not isinstance(test_content, Dict): + raise exceptions.TestCaseFormatError(f"Invalid teststep: {teststep}") + + # api in v2/v3 format, convert to v4 testcase + if "request" in test_content and "name" in test_content: + test_content = ensure_testcase_v4_api(test_content) + + test_content.setdefault("config", {})["path"] = ref_testcase_path + ref_testcase_python_abs_path = make_testcase(test_content) + + # override testcase export + ref_testcase_export: List = test_content["config"].get("export", []) + if ref_testcase_export: + step_export: List = teststep.setdefault("export", []) + step_export.extend(ref_testcase_export) + teststep["export"] = list(set(step_export)) + + # prepare ref testcase class name + ref_testcase_cls_name = pytest_files_made_cache_mapping[ + ref_testcase_python_abs_path + ] + teststep["testcase"] = ref_testcase_cls_name + + # prepare import ref testcase + 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, ".") + import_expr = f"from {ref_module_name} import TestCase{ref_testcase_cls_name} as {ref_testcase_cls_name}" + if import_expr not in imports_list: + imports_list.append(import_expr) + + testcase_path = convert_relative_project_root_dir(testcase_abs_path) + # current file compared to ProjectRootDir + diff_levels = len(testcase_path.split(os.sep)) + if len(imports_list) > 0 and diff_levels > 0: + parent = ".parent" * diff_levels + import_deps = f""" +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__){parent})) +""" + imports_list.insert(0, import_deps) + + data = { + "version": __version__, + "testcase_path": testcase_path, + "class_name": f"TestCase{testcase_cls_name}", + "imports_list": imports_list, + "config_chain_style": make_config_chain_style(config), + "skip": make_config_skip(config), + "parameters": config.get("parameters"), + "reference_testcase": any(step.get("testcase") for step in teststeps), + "teststeps_chain_style": [ + make_teststep_chain_style(step) for step in teststeps + ], + } + content = __TEMPLATE__.render(data) + + # ensure new file's directory exists + dir_path = os.path.dirname(testcase_python_abs_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + with open(testcase_python_abs_path, "w", encoding="utf-8") as f: + f.write(content) + + 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_abs_path}") + + return testcase_python_abs_path + + +def __make(tests_path: Text): + """make testcase(s) with testcase/folder absolute path + generated pytest file path will be cached in pytest_files_made_cache_mapping + + Args: + 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) + test_files.extend(files_list) + elif os.path.isfile(tests_path): + test_files.append(tests_path) + else: + raise exceptions.TestcaseNotFound(f"Invalid tests path: {tests_path}") + + for test_file in test_files: + if test_file.lower().endswith("_test.py"): + pytest_files_run_set.add(test_file) + continue + + try: + test_content = load_test_file(test_file) + except (exceptions.FileNotFound, exceptions.FileFormatError) as ex: + logger.warning(f"Invalid test file: {test_file}\n{type(ex).__name__}: {ex}") + continue + + if not isinstance(test_content, Dict): + logger.warning( + f"Invalid test file: {test_file}\n" + f"reason: test content not in dict format." + ) + continue + + # api in v2/v3 format, convert to v4 testcase + if "request" in test_content and "name" in test_content: + test_content = ensure_testcase_v4_api(test_content) + + if "config" not in test_content: + logger.warning( + f"Invalid testcase file: {test_file}\nreason: missing config part." + ) + continue + elif not isinstance(test_content["config"], Dict): + logger.warning( + f"Invalid testcase 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 + + # invalid format + if "teststeps" not in test_content: + logger.warning(f"Invalid testcase file: {test_file}") + + # testcase + try: + testcase_pytest_path = make_testcase(test_content) + pytest_files_run_set.add(testcase_pytest_path) + except exceptions.TestCaseFormatError as ex: + logger.warning( + f"Invalid testcase file: {test_file}\n{type(ex).__name__}: {ex}" + ) + continue + + +def main_make(tests_paths: List[Text]) -> List[Text]: + if not tests_paths: + return [] + + ga4_client.send_event("hmake") + + for tests_path in tests_paths: + tests_path = ensure_path_sep(tests_path) + if not os.path.isabs(tests_path): + tests_path = os.path.join(os.getcwd(), tests_path) + + try: + __make(tests_path) + except exceptions.MyBaseError as ex: + logger.error(ex) + sys.exit(1) + + # format pytest files + pytest_files_format_list = pytest_files_made_cache_mapping.keys() + format_pytest_with_black(*pytest_files_format_list) + + return list(pytest_files_run_set) + + +def init_make_parser(subparsers): + """make testcases: parse command line options and run commands.""" + parser = subparsers.add_parser( + "make", + help="Convert YAML/JSON testcases to pytest cases.", + ) + parser.add_argument( + "testcase_path", nargs="*", help="Specify YAML/JSON testcase file/folder path" + ) + + return parser diff --git a/httprunner/make_test.py b/httprunner/make_test.py new file mode 100644 index 0000000..f3a8032 --- /dev/null +++ b/httprunner/make_test.py @@ -0,0 +1,213 @@ +import os +import unittest + +from httprunner import loader +from httprunner.make import ( + main_make, + convert_testcase_path, + pytest_files_made_cache_mapping, + make_config_chain_style, + make_teststep_chain_style, + pytest_files_run_set, + ensure_file_abs_path_valid, +) + + +class TestMake(unittest.TestCase): + def setUp(self) -> None: + pytest_files_made_cache_mapping.clear() + pytest_files_run_set.clear() + loader.project_meta = None + self.data_dir = os.path.join(os.getcwd(), "examples", "data") + + def test_make_testcase(self): + path = ["examples/postman_echo/request_methods/request_with_variables.yml"] + testcase_python_list = main_make(path) + self.assertEqual( + testcase_python_list[0], + os.path.join( + os.getcwd(), + os.path.join( + "examples", + "postman_echo", + "request_methods", + "request_with_variables_test.py", + ), + ), + ) + + def test_make_testcase_with_ref(self): + path = [ + "examples/postman_echo/request_methods/request_with_testcase_reference.yml" + ] + testcase_python_list = main_make(path) + self.assertEqual(len(testcase_python_list), 1) + self.assertIn( + os.path.join( + os.getcwd(), + os.path.join( + "examples", + "postman_echo", + "request_methods", + "request_with_testcase_reference_test.py", + ), + ), + testcase_python_list, + ) + + with open( + os.path.join( + "examples", + "postman_echo", + "request_methods", + "request_with_testcase_reference_test.py", + ) + ) as f: + content = f.read() + self.assertIn( + """ +from request_methods.request_with_functions_test import ( + TestCaseRequestWithFunctions as RequestWithFunctions, +) +""", + content, + ) + self.assertIn( + ".call(RequestWithFunctions)", + content, + ) + + def test_make_testcase_folder(self): + path = ["examples/postman_echo/request_methods/"] + testcase_python_list = main_make(path) + self.assertIn( + os.path.join( + os.getcwd(), + os.path.join( + "examples", + "postman_echo", + "request_methods", + "request_with_functions_test.py", + ), + ), + testcase_python_list, + ) + + def test_ensure_file_path_valid(self): + self.assertEqual( + ensure_file_abs_path_valid(os.path.join(self.data_dir, "a-b.c", "2 3.yml")), + os.path.join(self.data_dir, "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(), "pyproject.toml")), + os.path.join(os.getcwd(), "pyproject.toml"), + ) + 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(self.data_dir, ".csv")), + os.path.join(self.data_dir, ".csv"), + ) + + def test_convert_testcase_path(self): + self.assertEqual( + convert_testcase_path(os.path.join(self.data_dir, "a-b.c", "2 3.yml")), + ( + os.path.join(self.data_dir, "a_b_c", "T2_3_test.py"), + "T23", + ), + ) + self.assertEqual( + convert_testcase_path(os.path.join(self.data_dir, "a-b.c", "中文case.yml")), + ( + os.path.join(self.data_dir, "a_b_c", "中文case_test.py"), + "中文Case", + ), + ) + + def test_make_config_chain_style(self): + config = { + "name": "request methods testcase: validate with functions", + "variables": {"foo1": "bar1", "foo2": 22}, + "base_url": "https://postman_echo.com", + "verify": False, + "path": "examples/postman_echo/request_methods/validate_with_functions_test.py", + } + self.assertEqual( + make_config_chain_style(config), + """Config("request methods testcase: validate with functions").variables(**{'foo1': 'bar1', 'foo2': 22}).base_url("https://postman_echo.com").verify(False)""", + ) + + def test_make_teststep_chain_style(self): + step = { + "name": "get with params", + "variables": { + "foo1": "bar1", + "foo2": 123, + "sum_v": "${sum_two(1, 2)}", + }, + "request": { + "method": "GET", + "url": "/get", + "params": {"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}, + "headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"}, + }, + "testcase": "CLS_LB(TestCaseDemo)CLS_RB", + "extract": { + "session_foo1": "body.args.foo1", + "session_foo2": "body.args.foo2", + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.args.sum_v", "3"]}, + ], + } + teststep_chain_style = make_teststep_chain_style(step) + self.assertEqual( + teststep_chain_style, + """Step(RunRequest("get with params").with_variables(**{'foo1': 'bar1', 'foo2': 123, 'sum_v': '${sum_two(1, 2)}'}).get("/get").with_params(**{'foo1': '$foo1', 'foo2': '$foo2', 'sum_v': '$sum_v'}).with_headers(**{'User-Agent': 'HttpRunner/${get_httprunner_version()}'}).extract().with_jmespath('body.args.foo1', 'session_foo1').with_jmespath('body.args.foo2', 'session_foo2').validate().assert_equal("status_code", 200).assert_equal("body.args.sum_v", "3"))""", + ) + + def test_make_requests_with_json_chain_style(self): + step = { + "name": "get with params", + "variables": { + "foo1": "bar1", + "foo2": 123, + "sum_v": "${sum_two(1, 2)}", + "myjson": {"name": "user", "password": "123456"}, + }, + "request": { + "method": "GET", + "url": "/get", + "params": {"foo1": "$foo1", "foo2": "$foo2", "sum_v": "$sum_v"}, + "headers": {"User-Agent": "HttpRunner/${get_httprunner_version()}"}, + "json": "$myjson", + }, + "testcase": "CLS_LB(TestCaseDemo)CLS_RB", + "extract": { + "session_foo1": "body.args.foo1", + "session_foo2": "body.args.foo2", + }, + "validate": [ + {"eq": ["status_code", 200]}, + {"eq": ["body.args.sum_v", "3"]}, + ], + } + teststep_chain_style = make_teststep_chain_style(step) + self.assertEqual( + teststep_chain_style, + """Step(RunRequest("get with params").with_variables(**{'foo1': 'bar1', 'foo2': 123, 'sum_v': '${sum_two(1, 2)}', 'myjson': {'name': 'user', 'password': '123456'}}).get("/get").with_params(**{'foo1': '$foo1', 'foo2': '$foo2', 'sum_v': '$sum_v'}).with_headers(**{'User-Agent': 'HttpRunner/${get_httprunner_version()}'}).with_json("$myjson").extract().with_jmespath('body.args.foo1', 'session_foo1').with_jmespath('body.args.foo2', 'session_foo2').validate().assert_equal("status_code", 200).assert_equal("body.args.sum_v", "3"))""", + ) diff --git a/httprunner/models.py b/httprunner/models.py new file mode 100644 index 0000000..7994240 --- /dev/null +++ b/httprunner/models.py @@ -0,0 +1,305 @@ +import os +from enum import Enum +from typing import Any, Callable, Dict, List, Text, Union + +from pydantic import BaseModel, Field, HttpUrl + +Name = Text +Url = Text +BaseUrl = Union[HttpUrl, Text] +VariablesMapping = Dict[Text, Any] +FunctionsMapping = Dict[Text, Callable] +Headers = Dict[Text, Text] +Cookies = Dict[Text, Text] +Verify = bool +Hooks = List[Union[Text, Dict[Text, Text]]] +Export = List[Text] +Validators = List[Dict] +Env = Dict[Text, Any] + + +class MethodEnum(Text, Enum): + GET = "GET" + POST = "POST" + PUT = "PUT" + DELETE = "DELETE" + HEAD = "HEAD" + OPTIONS = "OPTIONS" + PATCH = "PATCH" + + +class ProtoType(Enum): + Binary = 1 + CyBinary = 2 + Compact = 3 + Json = 4 + + +class TransType(Enum): + Buffered = 1 + CyBuffered = 2 + Framed = 3 + CyFramed = 4 + + +# configs for thrift rpc +class TConfigThrift(BaseModel): + psm: Text = None + env: Text = None + cluster: Text = None + target: Text = None + include_dirs: List[Text] = None + thrift_client: Any = None + timeout: int = 10 + idl_path: Text = None + method: Text = None + ip: Text = "127.0.0.1" + port: int = 9000 + service_name: Text = None + proto_type: ProtoType = ProtoType.Binary + trans_type: TransType = TransType.Buffered + + +# configs for db +class TConfigDB(BaseModel): + psm: Text = None + user: Text = None + password: Text = None + ip: Text = None + port: int = 3306 + database: Text = None + + +class TransportEnum(Text, Enum): + BUFFERED = "buffered" + FRAMED = "framed" + + +class TThriftRequest(BaseModel): + """rpc request model""" + + method: Text = "" + params: Dict = {} + thrift_client: Any = None + idl_path: Text = "" # idl local path + timeout: int = 10 # sec + transport: TransportEnum = TransportEnum.BUFFERED + include_dirs: List[Union[Text, None]] = [] # param of thriftpy2.load + target: Text = "" # tcp://{ip}:{port} or sd://psm?cluster=xx&env=xx + env: Text = "prod" + cluster: Text = "default" + psm: Text = "" + service_name: Text = None + ip: Text = None + port: int = None + proto_type: ProtoType = None + trans_type: TransType = None + + +class SqlMethodEnum(Text, Enum): + FETCHONE = "FETCHONE" + FETCHMANY = "FETCHMANY" + FETCHALL = "FETCHALL" + INSERT = "INSERT" + UPDATE = "UPDATE" + DELETE = "DELETE" + + +class TSqlRequest(BaseModel): + """sql request model""" + + db_config: TConfigDB = TConfigDB() + method: SqlMethodEnum = None + sql: Text = None + size: int = 0 # limit nums of sql result + + +class TConfig(BaseModel): + name: Name + verify: Verify = False + base_url: BaseUrl = "" + # Text: prepare variables in debugtalk.py, ${gen_variables()} + variables: Union[VariablesMapping, Text] = {} + parameters: Union[VariablesMapping, Text] = {} + # setup_hooks: Hooks = [] + # teardown_hooks: Hooks = [] + export: Export = [] + path: Text = None + # configs for other protocols + thrift: TConfigThrift = None + db: TConfigDB = TConfigDB() + + +class TRequest(BaseModel): + """requests.Request model""" + + method: MethodEnum + url: Url + params: Dict[Text, Text] = {} + headers: Headers = {} + req_json: Union[Dict, List, Text] = Field(None, alias="json") + data: Union[Text, Dict[Text, Any]] = None + cookies: Cookies = {} + timeout: float = 120 + allow_redirects: bool = True + verify: Verify = False + upload: Dict = {} # used for upload files + + +class TStep(BaseModel): + name: Name + request: Union[TRequest, None] = None + testcase: Union[Text, Callable, None] = None + variables: VariablesMapping = {} + setup_hooks: Hooks = [] + teardown_hooks: Hooks = [] + # used to extract request's response field + extract: VariablesMapping = {} + # used to export session variables from referenced testcase + export: Export = [] + validators: Validators = Field([], alias="validate") + validate_script: List[Text] = [] + retry_times: int = 0 + retry_interval: int = 0 # sec + thrift_request: Union[TThriftRequest, None] = None + sql_request: Union[TSqlRequest, None] = None + + +class TestCase(BaseModel): + config: TConfig + teststeps: List[TStep] + + +class ProjectMeta(BaseModel): + debugtalk_py: Text = "" # debugtalk.py file content + debugtalk_path: Text = "" # debugtalk.py file path + dot_env_path: Text = "" # .env file path + functions: FunctionsMapping = {} # functions defined in debugtalk.py + env: Env = {} + RootDir: Text = ( + os.getcwd() + ) # project root directory (ensure absolute), the path debugtalk.py located + + +class TestsMapping(BaseModel): + project_meta: ProjectMeta + testcases: List[TestCase] + + +class TestCaseTime(BaseModel): + start_at: float = 0 + start_at_iso_format: Text = "" + duration: float = 0 + + +class TestCaseInOut(BaseModel): + config_vars: VariablesMapping = {} + export_vars: Dict = {} + + +class RequestStat(BaseModel): + content_size: float = 0 + response_time_ms: float = 0 + elapsed_ms: float = 0 + + +class AddressData(BaseModel): + client_ip: Text = "N/A" + client_port: int = 0 + server_ip: Text = "N/A" + server_port: int = 0 + + +class RequestData(BaseModel): + method: MethodEnum = MethodEnum.GET + url: Url + headers: Headers = {} + cookies: Cookies = {} + body: Union[Text, bytes, List, Dict, None] = {} + + +class ResponseData(BaseModel): + status_code: int + headers: Dict + cookies: Cookies + encoding: Union[Text, None] = None + content_type: Text + body: Union[Text, bytes, List, Dict, None] + + +class ReqRespData(BaseModel): + request: RequestData + response: ResponseData + + +class SessionData(BaseModel): + """request session data, including request, response, validators and stat data""" + + success: bool = False + # in most cases, req_resps only contains one request & response + # while when 30X redirect occurs, req_resps will contain multiple request & response + req_resps: List[ReqRespData] = [] + stat: RequestStat = RequestStat() + address: AddressData = AddressData() + validators: Dict = {} + + +class StepResult(BaseModel): + """teststep data, each step maybe corresponding to one request or one testcase""" + + name: Text = "" # teststep name + step_type: Text = "" # teststep type, request or testcase + success: bool = False + data: Union[SessionData, List["StepResult"]] = None + elapsed: float = 0.0 # teststep elapsed time + content_size: float = 0 # response content size + export_vars: VariablesMapping = {} + attachment: Text = "" # teststep attachment + + +StepResult.update_forward_refs() + + +class IStep(object): + def name(self) -> str: + raise NotImplementedError + + def type(self) -> str: + raise NotImplementedError + + def struct(self) -> TStep: + raise NotImplementedError + + def run(self, runner) -> StepResult: + # runner: HttpRunner + raise NotImplementedError + + +class TestCaseSummary(BaseModel): + name: Text + success: bool + case_id: Text + time: TestCaseTime + in_out: TestCaseInOut = {} + log: Text = "" + step_results: List[StepResult] = [] + + +class PlatformInfo(BaseModel): + httprunner_version: Text + python_version: Text + platform: Text + + +class Stat(BaseModel): + total: int = 0 + success: int = 0 + fail: int = 0 + + +class TestSuiteSummary(BaseModel): + success: bool = False + stat: Stat = Stat() + time: TestCaseTime = TestCaseTime() + platform: PlatformInfo + testcases: List[TestCaseSummary] diff --git a/httprunner/parser.py b/httprunner/parser.py new file mode 100644 index 0000000..e1adb7b --- /dev/null +++ b/httprunner/parser.py @@ -0,0 +1,606 @@ +import ast +import builtins +import os +import re +from typing import Any, Callable, Dict, List, Set, Text +from urllib.parse import urlparse + +from loguru import logger + +from httprunner import exceptions, loader, utils +from httprunner.models import FunctionsMapping, VariablesMapping + +# use $$ to escape $ notation +dolloar_regex_compile = re.compile(r"\$\$") +# variable notation, e.g. ${var} or $var +# variable should start with a-zA-Z_ +variable_regex_compile = re.compile(r"\$\{([a-zA-Z_]\w*)\}|\$([a-zA-Z_]\w*)") +# function notation, e.g. ${func1($var_1, $var_3)} +function_regex_compile = re.compile(r"\$\{([a-zA-Z_]\w*)\(([\$\w\.\-/\s=,]*)\)\}") + + +def parse_string_value(str_value: Text) -> Any: + """parse string to number if possible + e.g. "123" => 123 + "12.2" => 12.3 + "abc" => "abc" + "$var" => "$var" + """ + try: + return ast.literal_eval(str_value) + except ValueError: + return str_value + except SyntaxError: + # e.g. $var, ${func} + return str_value + + +def build_url(base_url, step_url): + """prepend url with base_url unless it's already an absolute URL""" + o_step_url = urlparse(step_url) + if o_step_url.netloc != "": + # step url is absolute url + return step_url + + # step url is relative, based on base url + o_base_url = urlparse(base_url) + if o_base_url.netloc == "": + # missed base url + raise exceptions.ParamsError("base url missed!") + + path = o_base_url.path.rstrip("/") + "/" + o_step_url.path.lstrip("/") + o_step_url = ( + o_step_url._replace(scheme=o_base_url.scheme) + ._replace(netloc=o_base_url.netloc) + ._replace(path=path) + ) + return o_step_url.geturl() + + +def regex_findall_variables(raw_string: Text) -> List[Text]: + """extract all variable names from content, which is in format $variable + + Args: + raw_string (str): string content + + Returns: + list: variables list extracted from string content + + Examples: + >>> regex_findall_variables("$variable") + ["variable"] + + >>> regex_findall_variables("/blog/$postid") + ["postid"] + + >>> regex_findall_variables("/$var1/$var2") + ["var1", "var2"] + + >>> regex_findall_variables("abc") + [] + + """ + try: + match_start_position = raw_string.index("$", 0) + except ValueError: + return [] + + vars_list = [] + while match_start_position < len(raw_string): + + # Notice: notation priority + # $$ > $var + + # search $$ + dollar_match = dolloar_regex_compile.match(raw_string, match_start_position) + if dollar_match: + match_start_position = dollar_match.end() + continue + + # search variable like ${var} or $var + var_match = variable_regex_compile.match(raw_string, match_start_position) + if var_match: + var_name = var_match.group(1) or var_match.group(2) + vars_list.append(var_name) + match_start_position = var_match.end() + continue + + curr_position = match_start_position + try: + # find next $ location + match_start_position = raw_string.index("$", curr_position + 1) + except ValueError: + # break while loop + break + + return vars_list + + +def regex_findall_functions(content: Text) -> List[Text]: + """extract all functions from string content, which are in format ${fun()} + + Args: + content (str): string content + + Returns: + list: functions list extracted from string content + + Examples: + >>> regex_findall_functions("${func(5)}") + ["func(5)"] + + >>> regex_findall_functions("${func(a=1, b=2)}") + ["func(a=1, b=2)"] + + >>> regex_findall_functions("/api/1000?_t=${get_timestamp()}") + ["get_timestamp()"] + + >>> regex_findall_functions("/api/${add(1, 2)}") + ["add(1, 2)"] + + >>> regex_findall_functions("/api/${add(1, 2)}?_t=${get_timestamp()}") + ["add(1, 2)", "get_timestamp()"] + + """ + try: + return function_regex_compile.findall(content) + except TypeError as ex: + logger.error(f"regex findall functions error: {ex}") + return [] + + +def extract_variables(content: Any) -> Set: + """extract all variables in content recursively.""" + if isinstance(content, (list, set, tuple)): + variables = set() + for item in content: + variables = variables | extract_variables(item) + return variables + + elif isinstance(content, dict): + variables = set() + for key, value in content.items(): + variables = variables | extract_variables(value) + return variables + + elif isinstance(content, str): + return set(regex_findall_variables(content)) + + return set() + + +def parse_function_params(params: Text) -> Dict: + """parse function params to args and kwargs. + + Args: + params (str): function param in string + + Returns: + dict: function meta dict + + { + "args": [], + "kwargs": {} + } + + Examples: + >>> parse_function_params("") + {'args': [], 'kwargs': {}} + + >>> parse_function_params("5") + {'args': [5], 'kwargs': {}} + + >>> parse_function_params("1, 2") + {'args': [1, 2], 'kwargs': {}} + + >>> parse_function_params("a=1, b=2") + {'args': [], 'kwargs': {'a': 1, 'b': 2}} + + >>> parse_function_params("1, 2, a=3, b=4") + {'args': [1, 2], 'kwargs': {'a':3, 'b':4}} + + """ + function_meta = {"args": [], "kwargs": {}} + + params_str = params.strip() + if params_str == "": + return function_meta + + args_list = params_str.split(",") + for arg in args_list: + arg = arg.strip() + if "=" in arg: + key, value = arg.split("=") + function_meta["kwargs"][key.strip()] = parse_string_value(value.strip()) + else: + function_meta["args"].append(parse_string_value(arg)) + + return function_meta + + +def get_mapping_variable( + variable_name: Text, variables_mapping: VariablesMapping +) -> Any: + """get variable from variables_mapping. + + Args: + variable_name (str): variable name + variables_mapping (dict): variables mapping + + Returns: + mapping variable value. + + Raises: + exceptions.VariableNotFound: variable is not found. + + """ + # TODO: get variable from debugtalk module and environ + try: + return variables_mapping[variable_name] + except KeyError: + raise exceptions.VariableNotFound( + f"{variable_name} not found in {variables_mapping}" + ) + + +def get_mapping_function( + function_name: Text, functions_mapping: FunctionsMapping +) -> Callable: + """get function from functions_mapping, + if not found, then try to check if builtin function. + + Args: + function_name (str): function name + functions_mapping (dict): functions mapping + + Returns: + mapping function object. + + Raises: + exceptions.FunctionNotFound: function is neither defined in debugtalk.py nor builtin. + + """ + if function_name in functions_mapping: + return functions_mapping[function_name] + + elif function_name in ["parameterize", "P"]: + return loader.load_csv_file + + elif function_name in ["environ", "ENV"]: + return utils.get_os_environ + + elif function_name in ["multipart_encoder", "multipart_content_type"]: + # extension for upload test + from httprunner.ext import uploader + + return getattr(uploader, function_name) + + try: + # check if HttpRunner builtin functions + built_in_functions = loader.load_builtin_functions() + return built_in_functions[function_name] + except KeyError: + pass + + try: + # check if Python builtin functions + return getattr(builtins, function_name) + except AttributeError: + pass + + raise exceptions.FunctionNotFound(f"{function_name} is not found.") + + +def parse_string( + raw_string: Text, + variables_mapping: VariablesMapping, + functions_mapping: FunctionsMapping, +) -> Any: + """parse string content with variables and functions mapping. + + Args: + raw_string: raw string content to be parsed. + variables_mapping: variables mapping. + functions_mapping: functions mapping. + + Returns: + str: parsed string content. + + Examples: + >>> raw_string = "abc${add_one($num)}def" + >>> variables_mapping = {"num": 3} + >>> functions_mapping = {"add_one": lambda x: x + 1} + >>> parse_string(raw_string, variables_mapping, functions_mapping) + "abc4def" + + """ + try: + match_start_position = raw_string.index("$", 0) + parsed_string = raw_string[0:match_start_position] + except ValueError: + parsed_string = raw_string + return parsed_string + + while match_start_position < len(raw_string): + + # Notice: notation priority + # $$ > ${func($a, $b)} > $var + + # search $$ + dollar_match = dolloar_regex_compile.match(raw_string, match_start_position) + if dollar_match: + match_start_position = dollar_match.end() + parsed_string += "$" + continue + + # search function like ${func($a, $b)} + func_match = function_regex_compile.match(raw_string, match_start_position) + if func_match: + func_name = func_match.group(1) + func = get_mapping_function(func_name, functions_mapping) + + func_params_str = func_match.group(2) + 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) + + 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: + # raw_string is a function, e.g. "${add_one(3)}", return its eval value directly + return func_eval_value + + # raw_string contains one or many functions, e.g. "abc${add_one(3)}def" + parsed_string += str(func_eval_value) + match_start_position = func_match.end() + continue + + # search variable like ${var} or $var + var_match = variable_regex_compile.match(raw_string, match_start_position) + if var_match: + var_name = var_match.group(1) or var_match.group(2) + var_value = get_mapping_variable(var_name, variables_mapping) + + if f"${var_name}" == raw_string or "${" + var_name + "}" == raw_string: + # raw_string is a variable, $var or ${var}, return its value directly + return var_value + + # raw_string contains one or many variables, e.g. "abc${var}def" + parsed_string += str(var_value) + match_start_position = var_match.end() + continue + + curr_position = match_start_position + try: + # find next $ location + match_start_position = raw_string.index("$", curr_position + 1) + remain_string = raw_string[curr_position:match_start_position] + except ValueError: + remain_string = raw_string[curr_position:] + # break while loop + match_start_position = len(raw_string) + + parsed_string += remain_string + + return parsed_string + + +def parse_data( + raw_data: Any, + variables_mapping: VariablesMapping = None, + functions_mapping: FunctionsMapping = None, +) -> Any: + """parse raw data with evaluated variables mapping. + Notice: variables_mapping should not contain any variable or function. + """ + if isinstance(raw_data, str): + # content in string format may contains variables and functions + variables_mapping = variables_mapping or {} + functions_mapping = functions_mapping or {} + # only strip whitespaces and tabs, \n\r is left because they maybe used in changeset + raw_data = raw_data.strip(" \t") + return parse_string(raw_data, variables_mapping, functions_mapping) + + elif isinstance(raw_data, (list, set, tuple)): + return [ + parse_data(item, variables_mapping, functions_mapping) for item in raw_data + ] + + elif isinstance(raw_data, dict): + parsed_data = {} + for key, value in raw_data.items(): + parsed_key = parse_data(key, variables_mapping, functions_mapping) + parsed_value = parse_data(value, variables_mapping, functions_mapping) + parsed_data[parsed_key] = parsed_value + + return parsed_data + + else: + # other types, e.g. None, int, float, bool + return raw_data + + +def parse_variables_mapping( + variables_mapping: VariablesMapping, functions_mapping: FunctionsMapping = None +) -> VariablesMapping: + + parsed_variables: VariablesMapping = {} + + while len(parsed_variables) != len(variables_mapping): + for var_name in variables_mapping: + + if var_name in parsed_variables: + continue + + var_value = variables_mapping[var_name] + variables = extract_variables(var_value) + + # check if reference variable itself + if var_name in variables: + # e.g. + # variables_mapping = {"token": "abc$token"} + # variables_mapping = {"key": ["$key", 2]} + raise exceptions.VariableNotFound(var_name) + + # check if reference variable not in variables_mapping + not_defined_variables = [ + v_name for v_name in variables if v_name not in variables_mapping + ] + if not_defined_variables: + # e.g. {"varA": "123$varB", "varB": "456$varC"} + # e.g. {"varC": "${sum_two($a, $b)}"} + raise exceptions.VariableNotFound(not_defined_variables) + + try: + parsed_value = parse_data( + var_value, parsed_variables, functions_mapping + ) + except exceptions.VariableNotFound: + continue + + parsed_variables[var_name] = parsed_value + + return parsed_variables + + +def parse_parameters( + parameters: Dict, +) -> List[Dict]: + """parse parameters and generate cartesian product. + + Args: + parameters (Dict) parameters: parameter name and value mapping + parameter value may be in three types: + (1) data list, e.g. ["iOS/10.1", "iOS/10.2", "iOS/10.3"] + (2) call built-in parameterize function, "${parameterize(account.csv)}" + (3) call custom function in debugtalk.py, "${gen_app_version()}" + + Returns: + list: cartesian product list + + Examples: + >>> parameters = { + "user_agent": ["iOS/10.1", "iOS/10.2", "iOS/10.3"], + "username-password": "${parameterize(account.csv)}", + "app_version": "${gen_app_version()}", + } + >>> parse_parameters(parameters) + + """ + parsed_parameters_list: List[List[Dict]] = [] + + # load project_meta functions + project_meta = loader.load_project_meta(os.getcwd()) + functions_mapping = project_meta.functions + + for parameter_name, parameter_content in parameters.items(): + parameter_name_list = parameter_name.split("-") + + if isinstance(parameter_content, List): + # (1) data list + # e.g. {"app_version": ["2.8.5", "2.8.6"]} + # => [{"app_version": "2.8.5", "app_version": "2.8.6"}] + # e.g. {"username-password": [["user1", "111111"], ["test2", "222222"]} + # => [{"username": "user1", "password": "111111"}, {"username": "user2", "password": "222222"}] + parameter_content_list: List[Dict] = [] + for parameter_item in parameter_content: + if not isinstance(parameter_item, (list, tuple)): + # "2.8.5" => ["2.8.5"] + parameter_item = [parameter_item] + + # ["app_version"], ["2.8.5"] => {"app_version": "2.8.5"} + # ["username", "password"], ["user1", "111111"] => {"username": "user1", "password": "111111"} + parameter_content_dict = dict(zip(parameter_name_list, parameter_item)) + parameter_content_list.append(parameter_content_dict) + + elif isinstance(parameter_content, Text): + # (2) & (3) + parsed_parameter_content: List = parse_data( + parameter_content, {}, functions_mapping + ) + if not isinstance(parsed_parameter_content, List): + raise exceptions.ParamsError( + f"parameters content should be in List type, got {parsed_parameter_content} for {parameter_content}" + ) + + parameter_content_list: List[Dict] = [] + for parameter_item in parsed_parameter_content: + if isinstance(parameter_item, Dict): + # get subset by parameter name + # {"app_version": "${gen_app_version()}"} + # gen_app_version() => [{'app_version': '2.8.5'}, {'app_version': '2.8.6'}] + # {"username-password": "${get_account()}"} + # get_account() => [ + # {"username": "user1", "password": "111111"}, + # {"username": "user2", "password": "222222"} + # ] + parameter_dict: Dict = { + key: parameter_item[key] for key in parameter_name_list + } + elif isinstance(parameter_item, (List, tuple)): + if len(parameter_name_list) == len(parameter_item): + # {"username-password": "${get_account()}"} + # get_account() => [("user1", "111111"), ("user2", "222222")] + parameter_dict = dict(zip(parameter_name_list, parameter_item)) + else: + raise exceptions.ParamsError( + f"parameter names length are not equal to value length.\n" + f"parameter names: {parameter_name_list}\n" + f"parameter values: {parameter_item}" + ) + elif len(parameter_name_list) == 1: + # {"user_agent": "${get_user_agent()}"} + # get_user_agent() => ["iOS/10.1", "iOS/10.2"] + # parameter_dict will get: {"user_agent": "iOS/10.1", "user_agent": "iOS/10.2"} + parameter_dict = {parameter_name_list[0]: parameter_item} + else: + raise exceptions.ParamsError( + f"Invalid parameter names and values:\n" + f"parameter names: {parameter_name_list}\n" + f"parameter values: {parameter_item}" + ) + + parameter_content_list.append(parameter_dict) + + else: + raise exceptions.ParamsError( + f"parameter content should be List or Text(variables or functions call), got {parameter_content}" + ) + + parsed_parameters_list.append(parameter_content_list) + + return utils.gen_cartesian_product(*parsed_parameters_list) + + +class Parser(object): + def __init__(self, functions_mapping: FunctionsMapping = None) -> None: + self.functions_mapping = functions_mapping + + def parse_string( + self, raw_string: Text, variables_mapping: VariablesMapping + ) -> Any: + return parse_string(raw_string, variables_mapping, self.functions_mapping) + + def parse_variables(self, variables_mapping: VariablesMapping) -> VariablesMapping: + return parse_variables_mapping(variables_mapping, self.functions_mapping) + + def parse_data( + self, raw_data: Any, variables_mapping: VariablesMapping = None + ) -> Any: + return parse_data(raw_data, variables_mapping, self.functions_mapping) + + def get_mapping_function(self, func_name: Text) -> Callable: + return get_mapping_function(func_name, self.functions_mapping) diff --git a/httprunner/parser_test.py b/httprunner/parser_test.py new file mode 100644 index 0000000..2ac7542 --- /dev/null +++ b/httprunner/parser_test.py @@ -0,0 +1,574 @@ +import os +import time +import unittest + +from httprunner import parser +from httprunner.exceptions import FunctionNotFound, VariableNotFound +from httprunner.loader import load_project_meta + + +class TestParserBasic(unittest.TestCase): + def test_build_url(self): + url = parser.build_url("https://postman-echo.com", "/get") + self.assertEqual(url, "https://postman-echo.com/get") + url = parser.build_url("https://postman-echo.com", "get") + self.assertEqual(url, "https://postman-echo.com/get") + url = parser.build_url("https://postman-echo.com/", "/get") + self.assertEqual(url, "https://postman-echo.com/get") + + url = parser.build_url("https://postman-echo.com/abc/", "/get?a=1&b=2") + self.assertEqual(url, "https://postman-echo.com/abc/get?a=1&b=2") + url = parser.build_url("https://postman-echo.com/abc/", "get?a=1&b=2") + self.assertEqual(url, "https://postman-echo.com/abc/get?a=1&b=2") + + # omit query string in base url + url = parser.build_url("https://postman-echo.com/abc?x=6&y=9", "/get?a=1&b=2") + self.assertEqual(url, "https://postman-echo.com/abc/get?a=1&b=2") + + url = parser.build_url("", "https://postman-echo.com/get") + self.assertEqual(url, "https://postman-echo.com/get") + + # notice: step request url > config base url + url = parser.build_url("https://postman-echo.com", "https://httpbin.org/get") + self.assertEqual(url, "https://httpbin.org/get") + + def test_parse_variables_mapping(self): + variables = {"varA": "$varB", "varB": "$varC", "varC": "123", "a": 1, "b": 2} + parsed_variables = parser.parse_variables_mapping(variables) + print(parsed_variables) + self.assertEqual(parsed_variables["varA"], "123") + self.assertEqual(parsed_variables["varB"], "123") + + def test_parse_variables_mapping_exception(self): + variables = {"varA": "$varB", "varB": "$varC", "a": 1, "b": 2} + with self.assertRaises(VariableNotFound): + parser.parse_variables_mapping(variables) + + def test_parse_string_value(self): + self.assertEqual(parser.parse_string_value("123"), 123) + self.assertEqual(parser.parse_string_value("12.3"), 12.3) + self.assertEqual(parser.parse_string_value("a123"), "a123") + self.assertEqual(parser.parse_string_value("$var"), "$var") + self.assertEqual(parser.parse_string_value("${func}"), "${func}") + + def test_regex_findall_variables(self): + self.assertEqual(parser.regex_findall_variables("$variable"), ["variable"]) + self.assertEqual(parser.regex_findall_variables("${variable}123"), ["variable"]) + self.assertEqual(parser.regex_findall_variables("/blog/$postid"), ["postid"]) + self.assertEqual( + parser.regex_findall_variables("/$var1/$var2"), ["var1", "var2"] + ) + self.assertEqual(parser.regex_findall_variables("abc"), []) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$a"), ["a"]) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$a"), []) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$$a"), ["a"]) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$$$a"), []) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$a$b"), ["b"]) + self.assertEqual(parser.regex_findall_variables("Z:2>1*0*1+1$$a$$b"), []) + # variable should not start with digit + self.assertEqual(parser.regex_findall_variables("$1a"), []) + self.assertEqual(parser.regex_findall_variables("${1a}"), []) + + def test_extract_variables(self): + self.assertEqual(parser.extract_variables("$var"), {"var"}) + self.assertEqual(parser.extract_variables("$var123"), {"var123"}) + self.assertEqual(parser.extract_variables("$var_name"), {"var_name"}) + self.assertEqual(parser.extract_variables("var"), set()) + self.assertEqual(parser.extract_variables("a$var"), {"var"}) + self.assertEqual(parser.extract_variables("$v ar"), {"v"}) + self.assertEqual(parser.extract_variables(" "), set()) + self.assertEqual(parser.extract_variables("$abc*"), {"abc"}) + self.assertEqual(parser.extract_variables("${func()}"), set()) + self.assertEqual(parser.extract_variables("${func(1,2)}"), set()) + self.assertEqual( + parser.extract_variables("${gen_md5($TOKEN, $data, $random)}"), + {"TOKEN", "data", "random"}, + ) + self.assertEqual(parser.extract_variables("Z:2>1*0*1+1$$1"), set()) + + def test_parse_function_params(self): + self.assertEqual(parser.parse_function_params(""), {"args": [], "kwargs": {}}) + self.assertEqual(parser.parse_function_params("5"), {"args": [5], "kwargs": {}}) + self.assertEqual( + parser.parse_function_params("1, 2"), {"args": [1, 2], "kwargs": {}} + ) + self.assertEqual( + parser.parse_function_params("a=1, b=2"), + {"args": [], "kwargs": {"a": 1, "b": 2}}, + ) + self.assertEqual( + parser.parse_function_params("a= 1, b =2"), + {"args": [], "kwargs": {"a": 1, "b": 2}}, + ) + self.assertEqual( + parser.parse_function_params("1, 2, a=3, b=4"), + {"args": [1, 2], "kwargs": {"a": 3, "b": 4}}, + ) + self.assertEqual( + parser.parse_function_params("$request, 123"), + {"args": ["$request", 123], "kwargs": {}}, + ) + self.assertEqual(parser.parse_function_params(" "), {"args": [], "kwargs": {}}) + self.assertEqual( + parser.parse_function_params("hello world, a=3, b=4"), + {"args": ["hello world"], "kwargs": {"a": 3, "b": 4}}, + ) + self.assertEqual( + parser.parse_function_params("$request, 12 3"), + {"args": ["$request", "12 3"], "kwargs": {}}, + ) + + def test_extract_functions(self): + self.assertEqual(parser.regex_findall_functions("${func()}"), [("func", "")]) + self.assertEqual(parser.regex_findall_functions("${func(5)}"), [("func", "5")]) + self.assertEqual( + parser.regex_findall_functions("${func(a=1, b=2)}"), [("func", "a=1, b=2")] + ) + self.assertEqual( + parser.regex_findall_functions("${func(1, $b, c=$x, d=4)}"), + [("func", "1, $b, c=$x, d=4")], + ) + self.assertEqual( + parser.regex_findall_functions("/api/1000?_t=${get_timestamp()}"), + [("get_timestamp", "")], + ) + self.assertEqual( + parser.regex_findall_functions("/api/${add(1, 2)}"), [("add", "1, 2")] + ) + self.assertEqual( + parser.regex_findall_functions("/api/${add(1, 2)}?_t=${get_timestamp()}"), + [("add", "1, 2"), ("get_timestamp", "")], + ) + self.assertEqual( + parser.regex_findall_functions("abc${func(1, 2, a=3, b=4)}def"), + [("func", "1, 2, a=3, b=4")], + ) + + def test_parse_data_string_with_variables(self): + variables_mapping = { + "var_1": "abc", + "var_2": "def", + "var_3": 123, + "var_4": {"a": 1}, + "var_5": True, + "var_6": None, + } + self.assertEqual(parser.parse_data("$var_1", variables_mapping), "abc") + self.assertEqual(parser.parse_data("${var_1}", variables_mapping), "abc") + self.assertEqual(parser.parse_data("var_1", variables_mapping), "var_1") + self.assertEqual(parser.parse_data("$var_1#XYZ", variables_mapping), "abc#XYZ") + self.assertEqual( + parser.parse_data("${var_1}#XYZ", variables_mapping), "abc#XYZ" + ) + self.assertEqual( + parser.parse_data("/$var_1/$var_2/var3", variables_mapping), "/abc/def/var3" + ) + self.assertEqual(parser.parse_data("$var_3", variables_mapping), 123) + self.assertEqual(parser.parse_data("$var_4", variables_mapping), {"a": 1}) + self.assertEqual(parser.parse_data("$var_5", variables_mapping), True) + self.assertEqual(parser.parse_data("abc$var_5", variables_mapping), "abcTrue") + self.assertEqual( + parser.parse_data("abc$var_4", variables_mapping), "abc{'a': 1}" + ) + self.assertEqual(parser.parse_data("$var_6", variables_mapping), None) + + with self.assertRaises(VariableNotFound): + parser.parse_data("/api/$SECRET_KEY", variables_mapping) + + self.assertEqual( + parser.parse_data(["$var_1", "$var_2"], variables_mapping), ["abc", "def"] + ) + self.assertEqual( + parser.parse_data({"$var_1": "$var_2"}, variables_mapping), {"abc": "def"} + ) + + # format: $var + value = parser.parse_data("ABC$var_1", variables_mapping) + self.assertEqual(value, "ABCabc") + + value = parser.parse_data("ABC$var_1$var_3", variables_mapping) + self.assertEqual(value, "ABCabc123") + + value = parser.parse_data("ABC$var_1/$var_3", variables_mapping) + self.assertEqual(value, "ABCabc/123") + + value = parser.parse_data("ABC$var_1/", variables_mapping) + self.assertEqual(value, "ABCabc/") + + value = parser.parse_data("ABC$var_1$", variables_mapping) + self.assertEqual(value, "ABCabc$") + + value = parser.parse_data("ABC$var_1/123$var_1/456", variables_mapping) + self.assertEqual(value, "ABCabc/123abc/456") + + value = parser.parse_data("ABC$var_1/$var_2/$var_1", variables_mapping) + self.assertEqual(value, "ABCabc/def/abc") + + value = parser.parse_data("func1($var_1, $var_3)", variables_mapping) + self.assertEqual(value, "func1(abc, 123)") + + # format: ${var} + value = parser.parse_data("ABC${var_1}", variables_mapping) + self.assertEqual(value, "ABCabc") + + value = parser.parse_data("ABC${var_1}${var_3}", variables_mapping) + self.assertEqual(value, "ABCabc123") + + value = parser.parse_data("ABC${var_1}/${var_3}", variables_mapping) + self.assertEqual(value, "ABCabc/123") + + value = parser.parse_data("ABC${var_1}/", variables_mapping) + self.assertEqual(value, "ABCabc/") + + value = parser.parse_data("ABC${var_1}123", variables_mapping) + self.assertEqual(value, "ABCabc123") + + value = parser.parse_data("ABC${var_1}/123${var_1}/456", variables_mapping) + self.assertEqual(value, "ABCabc/123abc/456") + + value = parser.parse_data("ABC${var_1}/${var_2}/${var_1}", variables_mapping) + self.assertEqual(value, "ABCabc/def/abc") + + value = parser.parse_data("func1(${var_1}, ${var_3})", variables_mapping) + self.assertEqual(value, "func1(abc, 123)") + + def test_parse_data_multiple_identical_variables(self): + variables_mapping = { + "var_1": "abc", + "var_2": "def", + } + self.assertEqual( + parser.parse_data("/$var_1/$var_2/$var_1", variables_mapping), + "/abc/def/abc", + ) + + variables_mapping = {"userid": 100, "data": 1498} + content = "/users/$userid/training/$data?userId=$userid&data=$data" + self.assertEqual( + parser.parse_data(content, variables_mapping), + "/users/100/training/1498?userId=100&data=1498", + ) + + variables_mapping = {"user": 100, "userid": 1000, "data": 1498} + content = "/users/$user/$userid/$data?userId=$userid&data=$data" + self.assertEqual( + parser.parse_data(content, variables_mapping), + "/users/100/1000/1498?userId=1000&data=1498", + ) + + def test_parse_data_string_with_functions(self): + import random + import string + + functions_mapping = { + "gen_random_string": lambda str_len: "".join( + random.choice(string.ascii_letters + string.digits) + for _ in range(str_len) + ) + } + result = parser.parse_data( + "${gen_random_string(5)}", functions_mapping=functions_mapping + ) + self.assertEqual(len(result), 5) + + functions_mapping["add_two_nums"] = lambda a, b=1: a + b + self.assertEqual( + parser.parse_data( + "${add_two_nums(1)}", functions_mapping=functions_mapping + ), + 2, + ) + self.assertEqual( + parser.parse_data( + "${add_two_nums(1, 2)}", functions_mapping=functions_mapping + ), + 3, + ) + self.assertEqual( + parser.parse_data( + "/api/${add_two_nums(1, 2)}", functions_mapping=functions_mapping + ), + "/api/3", + ) + + with self.assertRaises(FunctionNotFound): + parser.parse_data("/api/${gen_md5(abc)}") + + variables_mapping = { + "var_1": "abc", + "var_2": "def", + "var_3": 123, + "var_4": {"a": 1}, + "var_5": True, + "var_6": None, + } + functions_mapping = {"func1": lambda x, y: str(x) + str(y)} + + value = parser.parse_data( + "${func1($var_1, $var_3)}", variables_mapping, functions_mapping + ) + self.assertEqual(value, "abc123") + + value = parser.parse_data( + "ABC${func1($var_1, $var_3)}DE", variables_mapping, functions_mapping + ) + self.assertEqual(value, "ABCabc123DE") + + value = parser.parse_data( + "ABC${func1($var_1, $var_3)}$var_5", variables_mapping, functions_mapping + ) + self.assertEqual(value, "ABCabc123True") + + value = parser.parse_data( + "ABC${func1($var_1, $var_3)}DE$var_4", variables_mapping, functions_mapping + ) + self.assertEqual(value, "ABCabc123DE{'a': 1}") + + value = parser.parse_data( + "ABC$var_5${func1($var_1, $var_3)}", variables_mapping, functions_mapping + ) + self.assertEqual(value, "ABCTrueabc123") + + value = parser.parse_data( + "ABC${ord(a)}DEF${len(abcd)}", variables_mapping, functions_mapping + ) + self.assertEqual(value, "ABC97DEF4") + + def test_parse_data_func_var_duplicate(self): + variables_mapping = { + "var_1": "abc", + "var_2": "def", + "var_3": 123, + "var_4": {"a": 1}, + "var_5": True, + "var_6": None, + } + functions_mapping = {"func1": lambda x, y: str(x) + str(y)} + value = parser.parse_data( + "ABC${func1($var_1, $var_3)}--${func1($var_1, $var_3)}", + variables_mapping, + functions_mapping, + ) + self.assertEqual(value, "ABCabc123--abc123") + + value = parser.parse_data( + "ABC${func1($var_1, $var_3)}$var_1", variables_mapping, functions_mapping + ) + self.assertEqual(value, "ABCabc123abc") + + value = parser.parse_data( + "ABC${func1($var_1, $var_3)}$var_1--${func1($var_1, $var_3)}$var_1", + variables_mapping, + functions_mapping, + ) + self.assertEqual(value, "ABCabc123abc--abc123abc") + + def test_parse_data_func_abnormal(self): + variables_mapping = { + "var_1": "abc", + "var_2": "def", + "var_3": 123, + "var_4": {"a": 1}, + "var_5": True, + "var_6": None, + } + functions_mapping = {"func1": lambda x, y: str(x) + str(y)} + + # { + value = parser.parse_data("ABC$var_1{", variables_mapping, functions_mapping) + self.assertEqual(value, "ABCabc{") + + value = parser.parse_data( + "{ABC$var_1{}a}", variables_mapping, functions_mapping + ) + self.assertEqual(value, "{ABCabc{}a}") + + value = parser.parse_data( + "AB{C$var_1{}a}", variables_mapping, functions_mapping + ) + self.assertEqual(value, "AB{Cabc{}a}") + + # } + value = parser.parse_data("ABC$var_1}", variables_mapping, functions_mapping) + self.assertEqual(value, "ABCabc}") + + # $$ + value = parser.parse_data("ABC$$var_1{", variables_mapping, functions_mapping) + self.assertEqual(value, "ABC$var_1{") + + # $$$ + value = parser.parse_data("ABC$$$var_1{", variables_mapping, functions_mapping) + self.assertEqual(value, "ABC$abc{") + + # $$$$ + value = parser.parse_data("ABC$$$$var_1{", variables_mapping, functions_mapping) + self.assertEqual(value, "ABC$$var_1{") + + # ${ + value = parser.parse_data("ABC$var_1${", variables_mapping, functions_mapping) + self.assertEqual(value, "ABCabc${") + + value = parser.parse_data("ABC$var_1${a", variables_mapping, functions_mapping) + self.assertEqual(value, "ABCabc${a") + + # $} + value = parser.parse_data("ABC$var_1$}a", variables_mapping, functions_mapping) + self.assertEqual(value, "ABCabc$}a") + + # }{ + value = parser.parse_data("ABC$var_1}{a", variables_mapping, functions_mapping) + self.assertEqual(value, "ABCabc}{a") + + # {} + value = parser.parse_data("ABC$var_1{}a", variables_mapping, functions_mapping) + self.assertEqual(value, "ABCabc{}a") + + def test_parse_data_request(self): + content = { + "request": { + "url": "/api/users/$uid", + "method": "$method", + "headers": {"token": "$token"}, + "data": { + "null": None, + "true": True, + "false": False, + "empty_str": "", + "value": "abc${add_one(3)}def", + }, + } + } + variables_mapping = {"uid": 1000, "method": "POST", "token": "abc123"} + functions_mapping = {"add_one": lambda x: x + 1} + result = parser.parse_data(content, variables_mapping, functions_mapping) + self.assertEqual("/api/users/1000", result["request"]["url"]) + self.assertEqual("abc123", result["request"]["headers"]["token"]) + self.assertEqual("POST", result["request"]["method"]) + self.assertIsNone(result["request"]["data"]["null"]) + self.assertTrue(result["request"]["data"]["true"]) + self.assertFalse(result["request"]["data"]["false"]) + self.assertEqual("", result["request"]["data"]["empty_str"]) + self.assertEqual("abc4def", result["request"]["data"]["value"]) + + def test_parse_data_testcase(self): + variables = { + "uid": "1000", + "random": "A2dEx", + "authorization": "a83de0ff8d2e896dbd8efb81ba14e17d", + "data": {"name": "user", "password": "123456"}, + } + functions = { + "add_two_nums": lambda a, b=1: a + b, + "get_timestamp": lambda: int(time.time() * 1000), + } + testcase_template = { + "url": "http://127.0.0.1:5000/api/users/$uid/${add_two_nums(1,2)}", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "authorization": "$authorization", + "random": "$random", + "sum": "${add_two_nums(1, 2)}", + }, + "body": "$data", + } + parsed_testcase = parser.parse_data(testcase_template, variables, functions) + self.assertEqual( + parsed_testcase["url"], "http://127.0.0.1:5000/api/users/1000/3" + ) + self.assertEqual( + parsed_testcase["headers"]["authorization"], variables["authorization"] + ) + self.assertEqual(parsed_testcase["headers"]["random"], variables["random"]) + self.assertEqual(parsed_testcase["body"], variables["data"]) + self.assertEqual(parsed_testcase["headers"]["sum"], 3) + + def test_parse_parameters_testcase(self): + parameters = { + "user_agent": ["iOS/10.1", "iOS/10.2"], + "username-password": "${parameterize(request_methods/account.csv)}", + "sum": "${calculate_two_nums(1, 2)}", + } + load_project_meta( + os.path.join( + os.path.dirname(os.path.dirname(__file__)), + "examples", + "postman_echo", + "request_methods", + ), + ) + parsed_params = parser.parse_parameters(parameters) + self.assertEqual(len(parsed_params), 2 * 3 * 2) + + self.assertIn( + { + "username": "test1", + "password": "111111", + "user_agent": "iOS/10.1", + "sum": 3, + }, + parsed_params, + ) + self.assertIn( + { + "username": "test1", + "password": "111111", + "user_agent": "iOS/10.1", + "sum": 1, + }, + parsed_params, + ) + self.assertIn( + { + "username": "test1", + "password": "111111", + "user_agent": "iOS/10.2", + "sum": 3, + }, + parsed_params, + ) + self.assertIn( + { + "username": "test1", + "password": "111111", + "user_agent": "iOS/10.2", + "sum": 1, + }, + parsed_params, + ) + self.assertIn( + { + "username": "test2", + "password": "222222", + "user_agent": "iOS/10.1", + "sum": 3, + }, + parsed_params, + ) + self.assertIn( + { + "username": "test2", + "password": "222222", + "user_agent": "iOS/10.1", + "sum": 1, + }, + parsed_params, + ) + self.assertIn( + { + "username": "test2", + "password": "222222", + "user_agent": "iOS/10.2", + "sum": 3, + }, + parsed_params, + ) + self.assertIn( + { + "username": "test2", + "password": "222222", + "user_agent": "iOS/10.2", + "sum": 1, + }, + parsed_params, + ) diff --git a/httprunner/response.py b/httprunner/response.py new file mode 100644 index 0000000..f898344 --- /dev/null +++ b/httprunner/response.py @@ -0,0 +1,309 @@ +from typing import Dict, Text, Any + +import jmespath +from jmespath.exceptions import JMESPathError +from loguru import logger + +from httprunner import exceptions +from httprunner.exceptions import ValidationFailure, ParamsError +from httprunner.models import VariablesMapping, Validators +from httprunner.parser import parse_string_value, Parser + + +def get_uniform_comparator(comparator: Text): + """convert comparator alias to uniform name""" + if comparator in ["eq", "equals", "equal"]: + return "equal" + elif comparator in ["lt", "less_than"]: + return "less_than" + elif comparator in ["le", "less_or_equals"]: + return "less_or_equals" + elif comparator in ["gt", "greater_than"]: + return "greater_than" + elif comparator in ["ge", "greater_or_equals"]: + return "greater_or_equals" + elif comparator in ["ne", "not_equal"]: + return "not_equal" + elif comparator in ["str_eq", "string_equals"]: + return "string_equals" + elif comparator in ["len_eq", "length_equal"]: + return "length_equal" + elif comparator in [ + "len_gt", + "length_greater_than", + ]: + return "length_greater_than" + elif comparator in [ + "len_ge", + "length_greater_or_equals", + ]: + return "length_greater_or_equals" + elif comparator in ["len_lt", "length_less_than"]: + return "length_less_than" + elif comparator in [ + "len_le", + "length_less_or_equals", + ]: + return "length_less_or_equals" + else: + return comparator + + +def uniform_validator(validator): + """unify validator + + Args: + validator (dict): validator maybe in two formats: + + format1: this is kept for compatibility with the previous versions. + {"check": "status_code", "comparator": "eq", "expect": 201, "message": "test"} + {"check": "status_code", "assert": "eq", "expect": 201, "msg": "test"} + format2: recommended new version, {assert: [check_item, expected_value, msg]} + {'eq': ['status_code', 201, "test"]} + + Returns + dict: validator info + + { + "check": "status_code", + "expect": 201, + "assert": "equal", + "message": "test + } + + """ + if not isinstance(validator, dict): + raise ParamsError(f"invalid validator: {validator}") + + if "check" in validator and "expect" in validator: + # format1 + check_item = validator["check"] + expect_value = validator["expect"] + + if "assert" in validator: + comparator = validator.get("assert") + else: + comparator = validator.get("comparator", "eq") + + if "msg" in validator: + message = validator.get("msg") + else: + message = validator.get("message", "") + + elif len(validator) == 1: + # format2 + comparator = list(validator.keys())[0] + compare_values = validator[comparator] + + if not isinstance(compare_values, list) or len(compare_values) not in [2, 3]: + raise ParamsError(f"invalid validator: {validator}") + + check_item = compare_values[0] + expect_value = compare_values[1] + if len(compare_values) == 3: + message = compare_values[2] + else: + # len(compare_values) == 2 + message = "" + + else: + raise ParamsError(f"invalid validator: {validator}") + + # uniform comparator, e.g. lt => less_than, eq => equals + assert_method = get_uniform_comparator(comparator) + + return { + "check": check_item, + "expect": expect_value, + "assert": assert_method, + "message": message, + } + + +class ResponseObjectBase(object): + def __init__(self, resp_obj, parser: Parser): + """initialize with a response object + + Args: + resp_obj (instance): requests.Response instance + + """ + self.resp_obj = resp_obj + self.parser = parser + self.validation_results: Dict = {} + + def extract( + self, + extractors: Dict[Text, Text], + variables_mapping: VariablesMapping = None, + ) -> Dict[Text, Any]: + if not extractors: + return {} + + extract_mapping = {} + for key, field in extractors.items(): + if "$" in field: + # field contains variable or function + field = self.parser.parse_data(field, variables_mapping) + field_value = self._search_jmespath(field) + extract_mapping[key] = field_value + + logger.info(f"extract mapping: {extract_mapping}") + return extract_mapping + + def _search_jmespath(self, expr: Text) -> Any: + try: + check_value = jmespath.search(expr, self.resp_obj) + except JMESPathError as ex: + logger.error( + f"failed to search with jmespath\n" + f"expression: {expr}\n" + f"data: {self.resp_obj}\n" + f"exception: {ex}" + ) + raise + return check_value + + def validate( + self, + validators: Validators, + variables_mapping: VariablesMapping = None, + ): + + variables_mapping = variables_mapping or {} + + self.validation_results = {} + if not validators: + return + + validate_pass = True + failures = [] + + for v in validators: + + if "validate_extractor" not in self.validation_results: + self.validation_results["validate_extractor"] = [] + + u_validator = uniform_validator(v) + + # check item + check_item = u_validator["check"] + if "$" in check_item: + # check_item is variable or function + check_item = self.parser.parse_data(check_item, variables_mapping) + check_item = parse_string_value(check_item) + + if check_item and isinstance(check_item, Text): + check_value = self._search_jmespath(check_item) + else: + # variable or function evaluation result is "" or not text + check_value = check_item + + # comparator + assert_method = u_validator["assert"] + assert_func = self.parser.get_mapping_function(assert_method) + + # expect item + expect_item = u_validator["expect"] + # parse expected value with config/teststep/extracted variables + expect_value = self.parser.parse_data(expect_item, variables_mapping) + + # message + message = u_validator["message"] + # parse message with config/teststep/extracted variables + message = self.parser.parse_data(message, variables_mapping) + + validate_msg = f"assert {check_item} {assert_method} {expect_value}({type(expect_value).__name__})" + + validator_dict = { + "comparator": assert_method, + "check": check_item, + "check_value": check_value, + "expect": expect_item, + "expect_value": expect_value, + "message": message, + } + + try: + assert_func(check_value, expect_value, message) + validate_msg += "\t==> pass" + logger.info(validate_msg) + validator_dict["check_result"] = "pass" + except AssertionError as ex: + validate_pass = False + validator_dict["check_result"] = "fail" + validate_msg += "\t==> fail" + validate_msg += ( + f"\n" + f"check_item: {check_item}\n" + f"check_value: {check_value}({type(check_value).__name__})\n" + f"assert_method: {assert_method}\n" + f"expect_value: {expect_value}({type(expect_value).__name__})" + ) + message = str(ex) + if message: + validate_msg += f"\nmessage: {message}" + + logger.error(validate_msg) + failures.append(validate_msg) + + self.validation_results["validate_extractor"].append(validator_dict) + + if not validate_pass: + failures_string = "\n".join([failure for failure in failures]) + raise ValidationFailure(failures_string) + + +class ResponseObject(ResponseObjectBase): + def __getattr__(self, key): + if key in ["json", "content", "body"]: + try: + value = self.resp_obj.json() + except ValueError: + value = self.resp_obj.content + elif key == "cookies": + value = self.resp_obj.cookies.get_dict() + else: + try: + value = getattr(self.resp_obj, key) + except AttributeError: + err_msg = "ResponseObject does not have attribute: {}".format(key) + logger.error(err_msg) + raise exceptions.ParamsError(err_msg) + + self.__dict__[key] = value + return value + + def _search_jmespath(self, expr: Text) -> Any: + resp_obj_meta = { + "status_code": self.status_code, + "headers": self.headers, + "cookies": self.cookies, + "body": self.body, + } + if not expr.startswith(tuple(resp_obj_meta.keys())): + if hasattr(self.resp_obj,expr): + return getattr(self.resp_obj,expr) + else: + return expr + + try: + check_value = jmespath.search(expr, resp_obj_meta) + except JMESPathError as ex: + logger.error( + f"failed to search with jmespath\n" + f"expression: {expr}\n" + f"data: {resp_obj_meta}\n" + f"exception: {ex}" + ) + raise + + return check_value + + +class ThriftResponseObject(ResponseObjectBase): + pass + + +class SqlResponseObject(ResponseObjectBase): + pass diff --git a/httprunner/response_test.py b/httprunner/response_test.py new file mode 100644 index 0000000..916b434 --- /dev/null +++ b/httprunner/response_test.py @@ -0,0 +1,90 @@ +import unittest + +import requests + +from httprunner.parser import Parser +from httprunner.response import ResponseObject, uniform_validator +from httprunner.utils import HTTP_BIN_URL + + +class TestResponse(unittest.TestCase): + def setUp(self) -> None: + resp = requests.post( + f"{HTTP_BIN_URL}/anything", + json={ + "locations": [ + {"name": "Seattle", "state": "WA"}, + {"name": "New York", "state": "NY"}, + {"name": "Bellevue", "state": "WA"}, + {"name": "Olympia", "state": "WA"}, + ] + }, + ) + parser = Parser( + functions_mapping={"get_name": lambda: "name", "get_num": lambda x: x} + ) + self.resp_obj = ResponseObject(resp, parser) + + def test_extract(self): + variables_mapping = {"body": "body"} + extract_mapping = self.resp_obj.extract( + { + "var_1": "body.json.locations[0]", + "var_2": "body.json.locations[3].name", + "var_3": "$body.json.locations[3].name", + "var_4": "$body.json.locations[3].${get_name()}", + }, + variables_mapping=variables_mapping, + ) + self.assertEqual(extract_mapping["var_1"], {"name": "Seattle", "state": "WA"}) + self.assertEqual(extract_mapping["var_2"], "Olympia") + self.assertEqual(extract_mapping["var_3"], "Olympia") + self.assertEqual(extract_mapping["var_4"], "Olympia") + + def test_validate(self): + self.resp_obj.validate( + [ + {"eq": ["body.json.locations[0].name", "Seattle"]}, + {"eq": ["body.json.locations[0]", {"name": "Seattle", "state": "WA"}]}, + ], + ) + + def test_validate_variables(self): + variables_mapping = {"index": 1, "var_empty": ""} + self.resp_obj.validate( + [ + {"eq": ["body.json.locations[$index].name", "New York"]}, + {"eq": ["$var_empty", ""]}, + ], + variables_mapping=variables_mapping, + ) + + def test_validate_functions(self): + variables_mapping = {"index": 1} + self.resp_obj.validate( + [ + {"eq": ["${get_num(0)}", 0]}, + {"eq": ["${get_num($index)}", 1]}, + ], + variables_mapping=variables_mapping, + ) + + def test_uniform_validator(self): + validators = [ + { + "check": "status_code", + "comparator": "eq", + "expect": 201, + "message": "test", + }, + {"check": "status_code", "assert": "eq", "expect": 201, "msg": "test"}, + {"eq": ["status_code", 201, "test"]}, + ] + expected = { + "check": "status_code", + "assert": "equal", + "expect": 201, + "message": "test", + } + for validator in validators: + self.assertEqual(uniform_validator(validator), expected) diff --git a/httprunner/runner.py b/httprunner/runner.py new file mode 100644 index 0000000..764c4fc --- /dev/null +++ b/httprunner/runner.py @@ -0,0 +1,248 @@ +import os +import time +import uuid +from datetime import datetime +from typing import Dict, List, Text + +try: + import allure + + ALLURE = allure +except ModuleNotFoundError: + ALLURE = None + +from loguru import logger + +from httprunner.client import HttpSession +from httprunner.config import Config +from httprunner.exceptions import ParamsError, ValidationFailure +from httprunner.loader import load_project_meta +from httprunner.models import ( + ProjectMeta, + StepResult, + TConfig, + TestCaseInOut, + TestCaseSummary, + TestCaseTime, + VariablesMapping, +) +from httprunner.parser import Parser +from httprunner.utils import LOGGER_FORMAT, merge_variables, ga4_client + + +class SessionRunner(object): + config: Config + teststeps: List[object] # list of Step + + parser: Parser = None + session: HttpSession = None + case_id: Text = "" + root_dir: Text = "" + thrift_client = None + db_engine = None + + __config: TConfig + __project_meta: ProjectMeta = None + __export: List[Text] = [] + __step_results: List[StepResult] = [] + __session_variables: VariablesMapping = {} + __is_referenced: bool = False + # time + __start_at: float = 0 + __duration: float = 0 + # log + __log_path: Text = "" + + def __init(self): + self.__config = self.config.struct() + self.__session_variables = self.__session_variables or {} + self.__start_at = 0 + self.__duration = 0 + self.__is_referenced = self.__is_referenced or False + + self.__project_meta = self.__project_meta or load_project_meta( + self.__config.path + ) + self.case_id = self.case_id or str(uuid.uuid4()) + self.root_dir = self.root_dir or self.__project_meta.RootDir + self.__log_path = os.path.join(self.root_dir, "logs", f"{self.case_id}.run.log") + + self.__step_results = self.__step_results or [] + self.session = self.session or HttpSession() + self.parser = self.parser or Parser(self.__project_meta.functions) + + def with_session(self, session: HttpSession) -> "SessionRunner": + self.session = session + return self + + def get_config(self) -> TConfig: + return self.__config + + def set_referenced(self) -> "SessionRunner": + self.__is_referenced = True + return self + + def with_case_id(self, case_id: Text) -> "SessionRunner": + self.case_id = case_id + return self + + def with_variables(self, variables: VariablesMapping) -> "SessionRunner": + self.__session_variables = variables + return self + + def with_export(self, export: List[Text]) -> "SessionRunner": + self.__export = export + return self + + def with_thrift_client(self, thrift_client) -> "SessionRunner": + self.thrift_client = thrift_client + return self + + def with_db_engine(self, db_engine) -> "SessionRunner": + self.db_engine = db_engine + return self + + def __parse_config(self, param: Dict = None) -> None: + # parse config variables + self.__config.variables.update(self.__session_variables) + if param: + self.__config.variables.update(param) + self.__config.variables = self.parser.parse_variables(self.__config.variables) + + # parse config name + self.__config.name = self.parser.parse_data( + self.__config.name, self.__config.variables + ) + + # parse config base url + self.__config.base_url = self.parser.parse_data( + self.__config.base_url, self.__config.variables + ) + + def get_export_variables(self) -> Dict: + # override testcase export vars with step export + export_var_names = self.__export or self.__config.export + export_vars_mapping = {} + for var_name in export_var_names: + if var_name not in self.__session_variables: + raise ParamsError( + f"failed to export variable {var_name} from session variables {self.__session_variables}" + ) + + export_vars_mapping[var_name] = self.__session_variables[var_name] + + return export_vars_mapping + + def get_summary(self) -> TestCaseSummary: + """get testcase result summary""" + start_at_timestamp = self.__start_at + start_at_iso_format = datetime.utcfromtimestamp(start_at_timestamp).isoformat() + + summary_success = True + for step_result in self.__step_results: + if not step_result.success: + summary_success = False + break + + return TestCaseSummary( + name=self.__config.name, + success=summary_success, + case_id=self.case_id, + time=TestCaseTime( + start_at=self.__start_at, + start_at_iso_format=start_at_iso_format, + duration=self.__duration, + ), + in_out=TestCaseInOut( + config_vars=self.__config.variables, + export_vars=self.get_export_variables(), + ), + log=self.__log_path, + step_results=self.__step_results, + ) + + def merge_step_variables(self, variables: VariablesMapping) -> VariablesMapping: + # override variables + # step variables > extracted variables from previous steps + variables = merge_variables(variables, self.__session_variables) + # step variables > testcase config variables + variables = merge_variables(variables, self.__config.variables) + + # parse variables + return self.parser.parse_variables(variables) + + def __run_step(self, step): + """run teststep, step maybe any kind that implements IStep interface + + Args: + step (Step): teststep + + """ + logger.info(f"run step begin: {step.name()} >>>>>>") + + # run step + for i in range(step.retry_times + 1): + try: + if ALLURE is not None: + with ALLURE.step(f"step: {step.name()}"): + step_result: StepResult = step.run(self) + else: + step_result: StepResult = step.run(self) + break + except ValidationFailure: + if i == step.retry_times: + raise + else: + logger.warning( + f"run step {step.name()} validation failed,wait {step.retry_interval} sec and try again" + ) + time.sleep(step.retry_interval) + logger.info( + f"run step retry ({i + 1}/{step.retry_times} time): {step.name()} >>>>>>" + ) + + # save extracted variables to session variables + self.__session_variables.update(step_result.export_vars) + # update testcase summary + self.__step_results.append(step_result) + + logger.info(f"run step end: {step.name()} <<<<<<\n") + + def test_start(self, param: Dict = None) -> "SessionRunner": + """main entrance, discovered by pytest""" + ga4_client.send_event("test_start") + print("\n") + self.__init() + self.__parse_config(param) + + if ALLURE is not None and not self.__is_referenced: + # update allure report meta + ALLURE.dynamic.title(self.__config.name) + ALLURE.dynamic.description(f"TestCase ID: {self.case_id}") + + logger.info( + f"Start to run testcase: {self.__config.name}, TestCase ID: {self.case_id}" + ) + + logger.add(self.__log_path, format=LOGGER_FORMAT, level="DEBUG") + self.__start_at = time.time() + try: + # run step in sequential order + for step in self.teststeps: + self.__run_step(step) + finally: + logger.info(f"generate testcase log: {self.__log_path}") + if ALLURE is not None: + ALLURE.attach.file( + self.__log_path, + name="all log", + attachment_type=ALLURE.attachment_type.TEXT, + ) + + self.__duration = time.time() - self.__start_at + return self + + +class HttpRunner(SessionRunner): + # split SessionRunner to keep consistent with golang version + pass diff --git a/httprunner/step.py b/httprunner/step.py new file mode 100644 index 0000000..7f0485a --- /dev/null +++ b/httprunner/step.py @@ -0,0 +1,67 @@ +from typing import Union + +from httprunner import HttpRunner +from httprunner.models import StepResult, TRequest, TStep, TestCase +from httprunner.step_request import ( + RequestWithOptionalArgs, + StepRequestExtraction, + StepRequestValidation, +) +from httprunner.step_sql_request import ( + RunSqlRequest, + StepSqlRequestExtraction, + StepSqlRequestValidation, +) +from httprunner.step_testcase import StepRefCase +from httprunner.step_thrift_request import ( + RunThriftRequest, + StepThriftRequestExtraction, + StepThriftRequestValidation, +) + + +class Step(object): + def __init__( + self, + step: Union[ + StepRequestValidation, + StepRequestExtraction, + RequestWithOptionalArgs, + StepRefCase, + RunSqlRequest, + StepSqlRequestValidation, + StepSqlRequestExtraction, + RunThriftRequest, + StepThriftRequestValidation, + StepThriftRequestExtraction, + ], + ): + self.__step = step + + @property + def request(self) -> TRequest: + return self.__step.struct().request + + @property + def testcase(self) -> TestCase: + return self.__step.struct().testcase + + @property + def retry_times(self) -> int: + return self.__step.struct().retry_times + + @property + def retry_interval(self) -> int: + return self.__step.struct().retry_interval + + def struct(self) -> TStep: + return self.__step.struct() + + def name(self) -> str: + return self.__step.name() + + def type(self) -> str: + return self.__step.type() + + def run(self, runner: HttpRunner) -> StepResult: + return self.__step.run(runner) diff --git a/httprunner/step_request.py b/httprunner/step_request.py new file mode 100644 index 0000000..2db1def --- /dev/null +++ b/httprunner/step_request.py @@ -0,0 +1,499 @@ +import json +import time +from typing import Any, Dict, List, Text, Union + +import requests +from loguru import logger + +from httprunner import utils +from httprunner.exceptions import ValidationFailure +from httprunner.ext.uploader import prepare_upload_step +from httprunner.models import ( + Hooks, + IStep, + MethodEnum, + StepResult, + TRequest, + TStep, + VariablesMapping, +) +from httprunner.parser import build_url, parse_variables_mapping +from httprunner.response import ResponseObject +from httprunner.runner import ALLURE, HttpRunner + + +def call_hooks( + runner: HttpRunner, hooks: Hooks, step_variables: VariablesMapping, hook_msg: Text +): + """call hook actions. + + Args: + hooks (list): each hook in hooks list maybe in two format. + + format1 (str): only call hook functions. + ${func()} + format2 (dict): assignment, the value returned by hook function will be assigned to variable. + {"var": "${func()}"} + + step_variables: current step variables to call hook, include two special variables + + request: parsed request dict + response: ResponseObject for current response + + hook_msg: setup/teardown request/testcase + + """ + logger.info(f"call hook actions: {hook_msg}") + + if not isinstance(hooks, List): + logger.error(f"Invalid hooks format: {hooks}") + return + + for hook in hooks: + if isinstance(hook, Text): + # format 1: ["${func()}"] + logger.debug(f"call hook function: {hook}") + runner.parser.parse_data(hook, step_variables) + elif isinstance(hook, Dict) and len(hook) == 1: + # format 2: {"var": "${func()}"} + var_name, hook_content = list(hook.items())[0] + hook_content_eval = runner.parser.parse_data(hook_content, step_variables) + logger.debug( + f"call hook function: {hook_content}, got value: {hook_content_eval}" + ) + logger.debug(f"assign variable: {var_name} = {hook_content_eval}") + step_variables[var_name] = hook_content_eval + else: + logger.error(f"Invalid hook format: {hook}") + + +def pretty_format(v) -> str: + if isinstance(v, dict): + return json.dumps(v, indent=4, ensure_ascii=False) + + if isinstance(v, requests.structures.CaseInsensitiveDict): + return json.dumps(dict(v.items()), indent=4, ensure_ascii=False) + + return repr(utils.omit_long_data(v)) + + +def run_step_request(runner: HttpRunner, step: TStep) -> StepResult: + """run teststep: request""" + step_result = StepResult( + name=step.name, + step_type="request", + success=False, + ) + start_time = time.time() + + # parse + functions = runner.parser.functions_mapping + step_variables = runner.merge_step_variables(step.variables) + prepare_upload_step(step, step_variables, functions) + # parse variables + step_variables = parse_variables_mapping(step_variables, functions) + + request_dict = step.request.dict() + request_dict.pop("upload", None) + parsed_request_dict = runner.parser.parse_data(request_dict, step_variables) + + request_headers = parsed_request_dict.pop("headers", {}) + # omit pseudo header names for HTTP/1, e.g. :authority, :method, :path, :scheme + request_headers = { + key: request_headers[key] for key in request_headers if not key.startswith(":") + } + request_headers[ + "HRUN-Request-ID" + ] = f"HRUN-{runner.case_id}-{str(int(time.time() * 1000))[-6:]}" + parsed_request_dict["headers"] = request_headers + + step_variables["request"] = parsed_request_dict + + # setup hooks + if step.setup_hooks: + call_hooks(runner, step.setup_hooks, step_variables, "setup request") + + # prepare arguments + config = runner.get_config() + method = parsed_request_dict.pop("method") + url_path = parsed_request_dict.pop("url") + url = build_url(config.base_url, url_path) + parsed_request_dict["verify"] = config.verify + parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {}) + + # log request + request_print = "====== request details ======\n" + request_print += f"url: {url}\n" + request_print += f"method: {method}\n" + for k, v in parsed_request_dict.items(): + request_print += f"{k}: {pretty_format(v)}\n" + + logger.debug(request_print) + if ALLURE is not None: + ALLURE.attach( + request_print, + name="request details", + attachment_type=ALLURE.attachment_type.TEXT, + ) + resp = runner.session.request(method, url, **parsed_request_dict) + + # log response + response_print = "====== response details ======\n" + response_print += f"status_code: {resp.status_code}\n" + response_print += f"headers: {pretty_format(resp.headers)}\n" + + try: + resp_body = resp.json() + except (requests.exceptions.JSONDecodeError, json.decoder.JSONDecodeError): + resp_body = resp.content + + response_print += f"body: {pretty_format(resp_body)}\n" + logger.debug(response_print) + if ALLURE is not None: + ALLURE.attach( + response_print, + name="response details", + attachment_type=ALLURE.attachment_type.TEXT, + ) + resp_obj = ResponseObject(resp, runner.parser) + step_variables["response"] = resp_obj + + # teardown hooks + if step.teardown_hooks: + call_hooks(runner, step.teardown_hooks, step_variables, "teardown request") + + # extract + extractors = step.extract + extract_mapping = resp_obj.extract(extractors, step_variables) + step_result.export_vars = extract_mapping + + variables_mapping = step_variables + variables_mapping.update(extract_mapping) + + # validate + validators = step.validators + try: + resp_obj.validate(validators, variables_mapping) + step_result.success = True + except ValidationFailure: + raise + finally: + session_data = runner.session.data + session_data.success = step_result.success + session_data.validators = resp_obj.validation_results + + # save step data + step_result.data = session_data + step_result.elapsed = time.time() - start_time + + return step_result + + +class StepRequestValidation(IStep): + def __init__(self, step: TStep): + self.__step = step + + def assert_equal( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append({"equal": [jmes_path, expected_value, message]}) + return self + + def assert_not_equal( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"not_equal": [jmes_path, expected_value, message]} + ) + return self + + def assert_greater_than( + self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"greater_than": [jmes_path, expected_value, message]} + ) + return self + + def assert_less_than( + self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"less_than": [jmes_path, expected_value, message]} + ) + return self + + def assert_greater_or_equals( + self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"greater_or_equals": [jmes_path, expected_value, message]} + ) + return self + + def assert_less_or_equals( + self, jmes_path: Text, expected_value: Union[int, float], message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"less_or_equals": [jmes_path, expected_value, message]} + ) + return self + + def assert_length_equal( + self, jmes_path: Text, expected_value: int, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"length_equal": [jmes_path, expected_value, message]} + ) + return self + + def assert_length_greater_than( + self, jmes_path: Text, expected_value: int, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"length_greater_than": [jmes_path, expected_value, message]} + ) + return self + + def assert_length_less_than( + self, jmes_path: Text, expected_value: int, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"length_less_than": [jmes_path, expected_value, message]} + ) + return self + + def assert_length_greater_or_equals( + self, jmes_path: Text, expected_value: int, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"length_greater_or_equals": [jmes_path, expected_value, message]} + ) + return self + + def assert_length_less_or_equals( + self, jmes_path: Text, expected_value: int, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"length_less_or_equals": [jmes_path, expected_value, message]} + ) + return self + + def assert_string_equals( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"string_equals": [jmes_path, expected_value, message]} + ) + return self + + def assert_startswith( + self, jmes_path: Text, expected_value: Text, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"startswith": [jmes_path, expected_value, message]} + ) + return self + + def assert_endswith( + self, jmes_path: Text, expected_value: Text, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"endswith": [jmes_path, expected_value, message]} + ) + return self + + def assert_regex_match( + self, jmes_path: Text, expected_value: Text, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"regex_match": [jmes_path, expected_value, message]} + ) + return self + + def assert_contains( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"contains": [jmes_path, expected_value, message]} + ) + return self + + def assert_contained_by( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"contained_by": [jmes_path, expected_value, message]} + ) + return self + + def assert_type_match( + self, jmes_path: Text, expected_value: Any, message: Text = "" + ) -> "StepRequestValidation": + self.__step.validators.append( + {"type_match": [jmes_path, expected_value, message]} + ) + return self + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return f"request-{self.__step.request.method}" + + def run(self, runner: HttpRunner): + return run_step_request(runner, self.__step) + + +class StepRequestExtraction(IStep): + def __init__(self, step: TStep): + self.__step = step + + def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction": + self.__step.extract[var_name] = jmes_path + return self + + # def with_regex(self): + # # TODO: extract response html with regex + # pass + # + # def with_jsonpath(self): + # # TODO: extract response json with jsonpath + # pass + + def validate(self) -> StepRequestValidation: + return StepRequestValidation(self.__step) + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return f"request-{self.__step.request.method}" + + def run(self, runner: HttpRunner): + return run_step_request(runner, self.__step) + + +class RequestWithOptionalArgs(IStep): + def __init__(self, step: TStep): + self.__step = step + + def with_params(self, **params) -> "RequestWithOptionalArgs": + self.__step.request.params.update(params) + return self + + def with_headers(self, **headers) -> "RequestWithOptionalArgs": + self.__step.request.headers.update(headers) + return self + + def with_cookies(self, **cookies) -> "RequestWithOptionalArgs": + self.__step.request.cookies.update(cookies) + return self + + def with_data(self, data) -> "RequestWithOptionalArgs": + self.__step.request.data = data + return self + + def with_json(self, req_json) -> "RequestWithOptionalArgs": + self.__step.request.req_json = req_json + return self + + def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs": + self.__step.request.timeout = timeout + return self + + def set_verify(self, verify: bool) -> "RequestWithOptionalArgs": + self.__step.request.verify = verify + return self + + def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs": + self.__step.request.allow_redirects = allow_redirects + return self + + def upload(self, **file_info) -> "RequestWithOptionalArgs": + self.__step.request.upload.update(file_info) + return self + + def teardown_hook( + self, hook: Text, assign_var_name: Text = None + ) -> "RequestWithOptionalArgs": + if assign_var_name: + self.__step.teardown_hooks.append({assign_var_name: hook}) + else: + self.__step.teardown_hooks.append(hook) + + return self + + def extract(self) -> StepRequestExtraction: + return StepRequestExtraction(self.__step) + + def validate(self) -> StepRequestValidation: + return StepRequestValidation(self.__step) + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return f"request-{self.__step.request.method}" + + def run(self, runner: HttpRunner): + return run_step_request(runner, self.__step) + + +class RunRequest(object): + def __init__(self, name: Text): + self.__step = TStep(name=name) + + def with_variables(self, **variables) -> "RunRequest": + self.__step.variables.update(variables) + return self + + def with_retry(self, retry_times, retry_interval) -> "RunRequest": + self.__step.retry_times = retry_times + self.__step.retry_interval = retry_interval + return self + + def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunRequest": + if assign_var_name: + self.__step.setup_hooks.append({assign_var_name: hook}) + else: + self.__step.setup_hooks.append(hook) + + return self + + def get(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.GET, url=url) + return RequestWithOptionalArgs(self.__step) + + def post(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.POST, url=url) + return RequestWithOptionalArgs(self.__step) + + def put(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.PUT, url=url) + return RequestWithOptionalArgs(self.__step) + + def head(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.HEAD, url=url) + return RequestWithOptionalArgs(self.__step) + + def delete(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.DELETE, url=url) + return RequestWithOptionalArgs(self.__step) + + def options(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.OPTIONS, url=url) + return RequestWithOptionalArgs(self.__step) + + def patch(self, url: Text) -> RequestWithOptionalArgs: + self.__step.request = TRequest(method=MethodEnum.PATCH, url=url) + return RequestWithOptionalArgs(self.__step) diff --git a/httprunner/step_request_test.py b/httprunner/step_request_test.py new file mode 100644 index 0000000..58164ac --- /dev/null +++ b/httprunner/step_request_test.py @@ -0,0 +1,17 @@ +import unittest + +from examples.postman_echo.request_methods.request_with_functions_test import ( + TestCaseRequestWithFunctions, +) + + +class TestRunRequest(unittest.TestCase): + def test_run_request(self): + runner = TestCaseRequestWithFunctions().test_start() + summary = runner.get_summary() + self.assertTrue(summary.success) + self.assertEqual(summary.name, "request methods testcase with functions") + self.assertEqual(len(summary.step_results), 3) + self.assertEqual(summary.step_results[0].name, "get with params") + self.assertEqual(summary.step_results[1].name, "post raw text") + self.assertEqual(summary.step_results[2].name, "post form data") diff --git a/httprunner/step_sql_request.py b/httprunner/step_sql_request.py new file mode 100644 index 0000000..47a5405 --- /dev/null +++ b/httprunner/step_sql_request.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +import sys +import time +from typing import Text + +from loguru import logger + +from httprunner import utils +from httprunner.exceptions import SqlMethodNotSupport, ValidationFailure +from httprunner.models import IStep, SqlMethodEnum, StepResult, TSqlRequest, TStep +from httprunner.response import SqlResponseObject +from httprunner.runner import ALLURE, HttpRunner +from httprunner.step_request import ( + StepRequestExtraction, + StepRequestValidation, + call_hooks, +) + +try: + import pymysql + import sqlalchemy + + SQL_READY = True +except ModuleNotFoundError: + SQL_READY = False + + +def ensure_sql_ready(): + if SQL_READY: + return + + msg = """ + uploader extension dependencies uninstalled, install first and try again. + install with pip: + $ pip install sqlalchemy pymysql + + or you can install httprunner with optional upload dependencies: + $ pip install "httprunner[sql]" + """ + logger.error(msg) + sys.exit(1) + + +def run_step_sql_request(runner: HttpRunner, step: TStep) -> StepResult: + """run teststep:sql request""" + start_time = time.time() + + step_result = StepResult( + name=step.name, + step_type="sql", + success=False, + ) + step_variables = runner.merge_step_variables(step.variables) + # parse + request_dict = step.sql_request.dict() + parsed_request_dict = runner.parser.parse_data(request_dict, step_variables) + config = runner.get_config() + parsed_request_dict["db_config"]["psm"] = ( + parsed_request_dict["db_config"]["psm"] or config.db.psm + ) + parsed_request_dict["db_config"]["user"] = ( + parsed_request_dict["db_config"]["user"] or config.db.user + ) + parsed_request_dict["db_config"]["password"] = ( + parsed_request_dict["db_config"]["password"] or config.db.password + ) + parsed_request_dict["db_config"]["ip"] = ( + parsed_request_dict["db_config"]["ip"] or config.db.ip + ) + parsed_request_dict["db_config"]["port"] = ( + parsed_request_dict["db_config"]["port"] or config.db.port + ) + parsed_request_dict["db_config"]["database"] = ( + parsed_request_dict["db_config"]["database"] or config.db.database + ) + + if not runner.db_engine: + ensure_sql_ready() + from httprunner.database.engine import DBEngine + + runner.db_engine = DBEngine( + f'mysql+pymysql://{parsed_request_dict["db_config"]["user"]}:' + f'{parsed_request_dict["db_config"]["password"]}@{parsed_request_dict["db_config"]["ip"]}:' + f'{parsed_request_dict["db_config"]["port"]}/{parsed_request_dict["db_config"]["database"]}' + f"?charset=utf8mb4" + ) + + # parsed_request_dict["headers"].setdefault( + # "HRUN-Request-ID", + # f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}", + # ) + + # setup hooks + if step.setup_hooks: + call_hooks(runner, step.setup_hooks, step_variables, "setup request") + + # log request + sql_request_print = "====== sql request details ======\n" + sql_request_print += f"sql: {step.sql_request.sql}\n" + for k, v in parsed_request_dict.items(): + v = utils.omit_long_data(v) + sql_request_print += f"{k}: {repr(v)}\n" + + sql_request_print += "\n" + + if ALLURE is not None: + ALLURE.attach( + sql_request_print, + name="sql request details", + attachment_type=ALLURE.attachment_type.TEXT, + ) + logger.info(f"Executing SQL: {parsed_request_dict['sql']}") + if step.sql_request.method == SqlMethodEnum.FETCHONE: + sql_resp = runner.db_engine.fetchone(parsed_request_dict["sql"]) + elif step.sql_request.method == SqlMethodEnum.INSERT: + sql_resp = runner.db_engine.insert(parsed_request_dict["sql"]) + elif step.sql_request.method == SqlMethodEnum.FETCHMANY: + sql_resp = runner.db_engine.fetchmany( + parsed_request_dict["sql"], parsed_request_dict["size"] + ) + elif step.sql_request.method == SqlMethodEnum.FETCHALL: + sql_resp = runner.db_engine.fetchall(parsed_request_dict["sql"]) + elif step.sql_request.method == SqlMethodEnum.UPDATE: + sql_resp = runner.db_engine.update(parsed_request_dict["sql"]) + elif step.sql_request.method == SqlMethodEnum.DELETE: + sql_resp = runner.db_engine.delete(parsed_request_dict["sql"]) + else: + raise SqlMethodNotSupport( + f"step.sql_request.method {parsed_request_dict['method']} not support" + ) + + # log response + sql_response_print = "====== sql response details ======\n" + if isinstance(sql_resp, dict): + for k, v in sql_resp.items(): + v = utils.omit_long_data(v) + sql_response_print += f"{k}: {repr(v)}\n" + elif isinstance(sql_resp, list): + sql_response_print += f"count: {len(sql_resp)}\n" + sql_response_print += "-" * 34 + "\n" + for el in sql_resp: + for k, v in el.items(): + v = utils.omit_long_data(v) + sql_response_print += f"{k}: {repr(v)}\n" + sql_response_print += "-" * 34 + "\n" + elif sql_resp is None: + sql_response_print += "None\n" + if ALLURE is not None: + ALLURE.attach( + sql_response_print, + name="sql response details", + attachment_type=ALLURE.attachment_type.TEXT, + ) + + resp_obj = SqlResponseObject(sql_resp, parser=runner.parser) + step_variables["sql_response"] = resp_obj + + # teardown hooks + if step.teardown_hooks: + call_hooks(runner, step.teardown_hooks, step_variables, "teardown request") + + def log_sql_req_resp_details(): + err_msg = "\n{} SQL DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32) + err_msg += sql_request_print + sql_response_print + logger.error(err_msg) + + # extract + extractors = step.extract + extract_mapping = resp_obj.extract(extractors) + step_result.export_vars = extract_mapping + + variables_mapping = step_variables + variables_mapping.update(extract_mapping) + + # validate + validators = step.validators + try: + resp_obj.validate(validators, variables_mapping) + step_result.success = True + except ValidationFailure: + log_sql_req_resp_details() + raise + finally: + session_data = runner.session.data + session_data.success = step_result.success + session_data.validators = resp_obj.validation_results + + # save step data + step_result.data = session_data + step_result.elapsed = time.time() - start_time + return step_result + + +class StepSqlRequestValidation(StepRequestValidation): + def __init__(self, step: TStep): + self.__step = step + super().__init__(step) + + def run(self, runner: HttpRunner): + return run_step_sql_request(runner, self.__step) + + +class StepSqlRequestExtraction(StepRequestExtraction): + def __init__(self, step: TStep): + self.__step = step + super().__init__(step) + + def run(self, runner: HttpRunner): + return run_step_sql_request(runner, self.__step) + + def validate(self) -> StepSqlRequestValidation: + return StepSqlRequestValidation(self.__step) + + +class RunSqlRequest(IStep): + def __init__(self, name: Text): + self.__step = TStep(name=name) + self.__step.sql_request = TSqlRequest() + + def with_variables(self, **variables) -> "RunSqlRequest": + self.__step.variables.update(variables) + return self + + def with_db_config( + self, user=None, password=None, ip=None, port=None, database=None, psm=None + ): + if user: + self.__step.sql_request.db_config.user = user + if password: + self.__step.sql_request.db_config.password = password + if ip: + self.__step.sql_request.db_config.ip = ip + if port: + self.__step.sql_request.db_config.port = port + if database: + self.__step.sql_request.db_config.database = database + if psm: + self.__step.sql_request.db_config.psm = psm + return self + + def fetchone(self, sql) -> "RunSqlRequest": + self.__step.sql_request.method = SqlMethodEnum.FETCHONE + self.__step.sql_request.sql = sql + return self + + def fetchmany(self, sql, size) -> "RunSqlRequest": + self.__step.sql_request.method = SqlMethodEnum.FETCHMANY + self.__step.sql_request.sql = sql + self.__step.sql_request.size = size + return self + + def fetchall(self, sql) -> "RunSqlRequest": + self.__step.sql_request.method = SqlMethodEnum.FETCHALL + self.__step.sql_request.sql = sql + return self + + def update(self, sql) -> "RunSqlRequest": + self.__step.sql_request.method = SqlMethodEnum.UPDATE + self.__step.sql_request.sql = sql + return self + + def delete(self, sql) -> "RunSqlRequest": + self.__step.sql_request.method = SqlMethodEnum.DELETE + self.__step.sql_request.sql = sql + return self + + def insert(self, sql) -> "RunSqlRequest": + self.__step.sql_request.method = SqlMethodEnum.INSERT + self.__step.sql_request.sql = sql + return self + + def with_retry(self, retry_times, retry_interval) -> "RunSqlRequest": + self.__step.retry_times = retry_times + self.__step.retry_interval = retry_interval + return self + + def teardown_hook( + self, hook: Text, assign_var_name: Text = None + ) -> "RunSqlRequest": + if assign_var_name: + self.__step.teardown_hooks.append({assign_var_name: hook}) + else: + self.__step.teardown_hooks.append(hook) + + return self + + def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunSqlRequest": + if assign_var_name: + self.__step.setup_hooks.append({assign_var_name: hook}) + else: + self.__step.setup_hooks.append(hook) + + return self + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return f"sql-request-{self.__step.sql_request.sql}" + + def run(self, runner) -> StepResult: + return run_step_sql_request(runner, self.__step) + + def extract(self) -> StepSqlRequestExtraction: + return StepSqlRequestExtraction(self.__step) + + def validate(self) -> StepSqlRequestValidation: + return StepSqlRequestValidation(self.__step) + + def with_jmespath( + self, jmes_path: Text, var_name: Text + ) -> "StepSqlRequestExtraction": + self.__step.extract[var_name] = jmes_path + return StepSqlRequestExtraction(self.__step) diff --git a/httprunner/step_testcase.py b/httprunner/step_testcase.py new file mode 100644 index 0000000..1341cc6 --- /dev/null +++ b/httprunner/step_testcase.py @@ -0,0 +1,103 @@ +from typing import Callable, Text + +from loguru import logger + +from httprunner import exceptions +from httprunner.models import IStep, StepResult, TStep, TestCaseSummary +from httprunner.runner import HttpRunner +from httprunner.step_request import call_hooks + + +def run_step_testcase(runner: HttpRunner, step: TStep) -> StepResult: + """run teststep: referenced testcase""" + step_result = StepResult(name=step.name, step_type="testcase") + step_variables = runner.merge_step_variables(step.variables) + step_export = step.export + + # setup hooks + if step.setup_hooks: + call_hooks(runner, step.setup_hooks, step_variables, "setup testcase") + + # TODO: override testcase with current step name/variables/export + + # step.testcase is a referenced testcase, e.g. RequestWithFunctions + ref_case_runner = step.testcase() + ref_case_runner.set_referenced().with_session(runner.session).with_case_id( + runner.case_id + ).with_variables(step_variables).with_export(step_export).test_start() + + # teardown hooks + if step.teardown_hooks: + call_hooks(runner, step.teardown_hooks, step.variables, "teardown testcase") + + summary: TestCaseSummary = ref_case_runner.get_summary() + step_result.data = summary.step_results # list of step data + step_result.export_vars = summary.in_out.export_vars + step_result.success = summary.success + + if step_result.export_vars: + logger.info(f"export variables: {step_result.export_vars}") + + return step_result + + +class StepRefCase(IStep): + def __init__(self, step: TStep): + self.__step = step + + def teardown_hook(self, hook: Text, assign_var_name: Text = None) -> "StepRefCase": + if assign_var_name: + self.__step.teardown_hooks.append({assign_var_name: hook}) + else: + self.__step.teardown_hooks.append(hook) + + return self + + def export(self, *var_name: Text) -> "StepRefCase": + self.__step.export.extend(var_name) + return self + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return f"request-{self.__step.request.method}" + + def run(self, runner: HttpRunner): + return run_step_testcase(runner, self.__step) + + +class RunTestCase(object): + def __init__(self, name: Text): + self.__step = TStep(name=name) + + def with_variables(self, **variables) -> "RunTestCase": + self.__step.variables.update(variables) + return self + + def with_retry(self, retry_times, retry_interval) -> "RunTestCase": + self.__step.retry_times = retry_times + self.__step.retry_interval = retry_interval + return self + + def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunTestCase": + if assign_var_name: + self.__step.setup_hooks.append({assign_var_name: hook}) + else: + self.__step.setup_hooks.append(hook) + + return self + + def call(self, testcase: Callable) -> StepRefCase: + if issubclass(testcase, HttpRunner): + # referenced testcase object + self.__step.testcase = testcase + else: + raise exceptions.ParamsError( + f"Invalid teststep referenced testcase: {testcase}" + ) + + return StepRefCase(self.__step) diff --git a/httprunner/step_testcase_test.py b/httprunner/step_testcase_test.py new file mode 100644 index 0000000..40c2c59 --- /dev/null +++ b/httprunner/step_testcase_test.py @@ -0,0 +1,27 @@ +import unittest + +from httprunner.runner import HttpRunner +from httprunner.step_testcase import RunTestCase +from examples.postman_echo.request_methods.request_with_functions_test import ( + TestCaseRequestWithFunctions, +) + + +class TestRunTestCase(unittest.TestCase): + def setUp(self): + self.runner = TestCaseRequestWithFunctions() + self.runner.test_start() + + def test_run_testcase_by_path(self): + + step_result = ( + RunTestCase("run referenced testcase") + .call(TestCaseRequestWithFunctions) + .run(self.runner) + ) + self.assertTrue(step_result.success) + self.assertEqual(step_result.name, "run referenced testcase") + self.assertEqual(len(step_result.data), 3) + self.assertEqual(step_result.data[0].name, "get with params") + self.assertEqual(step_result.data[1].name, "post raw text") + self.assertEqual(step_result.data[2].name, "post form data") diff --git a/httprunner/step_thrift_request.py b/httprunner/step_thrift_request.py new file mode 100644 index 0000000..322d34e --- /dev/null +++ b/httprunner/step_thrift_request.py @@ -0,0 +1,309 @@ +# -*- coding: utf-8 -*- +import platform +import sys +import time +from typing import Text, Union + +from loguru import logger + +from httprunner import utils +from httprunner.exceptions import ValidationFailure +from httprunner.models import ( + IStep, + ProtoType, + StepResult, + TransType, + TStep, + TThriftRequest, +) +from httprunner.response import ThriftResponseObject +from httprunner.runner import ALLURE, HttpRunner +from httprunner.step_request import ( + StepRequestExtraction, + StepRequestValidation, + call_hooks, +) + +try: + import thriftpy2 + + from thrift.Thrift import TType + + THRIFT_READY = True +except ModuleNotFoundError: + THRIFT_READY = False + + +def ensure_thrift_ready(): + assert platform.system() != "Windows", "Sorry,thrift not support Windows for now" + if THRIFT_READY: + return + + msg = """ + uploader extension dependencies uninstalled, install first and try again. + install with pip: + $ pip install cython thriftpy2 thrift + + or you can install httprunner with optional upload dependencies: + $ pip install "httprunner[thrift]" + """ + logger.error(msg) + sys.exit(1) + + +def run_step_thrift_request(runner: HttpRunner, step: TStep) -> StepResult: + """run teststep:thrift request""" + start_time = time.time() + + step_result = StepResult( + name=step.name, + step_type="thrift", + success=False, + ) + step_variables = runner.merge_step_variables(step.variables) + # parse + request_dict = step.thrift_request.dict() + parsed_request_dict = runner.parser.parse_data(request_dict, step_variables) + config = runner.get_config() + parsed_request_dict["psm"] = parsed_request_dict["psm"] or config.thrift.psm + parsed_request_dict["env"] = parsed_request_dict["env"] or config.thrift.env + parsed_request_dict["cluster"] = ( + parsed_request_dict["cluster"] or config.thrift.cluster + ) + parsed_request_dict["idl_path"] = ( + parsed_request_dict["idl_path"] or config.thrift.idl_path + ) + parsed_request_dict["include_dirs"] = ( + parsed_request_dict["include_dirs"] or config.thrift.include_dirs + ) + parsed_request_dict["method"] = ( + parsed_request_dict["method"] or config.thrift.method + ) + parsed_request_dict["service_name"] = ( + parsed_request_dict["service_name"] or config.thrift.service_name + ) + parsed_request_dict["ip"] = parsed_request_dict["ip"] or config.thrift.ip + parsed_request_dict["port"] = parsed_request_dict["port"] or config.thrift.port + parsed_request_dict["proto_type"] = ( + parsed_request_dict["proto_type"] or config.thrift.proto_type + ) + parsed_request_dict["trans_port"] = ( + parsed_request_dict["trans_type"] or config.thrift.trans_type + ) + parsed_request_dict["timeout"] = ( + parsed_request_dict["timeout"] or config.thrift.timeout + ) + parsed_request_dict["thrift_client"] = parsed_request_dict["thrift_client"] + + # parsed_request_dict["headers"].setdefault( + # "HRUN-Request-ID", + # f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}", + # ) + step_variables["thrift_request"] = parsed_request_dict + + psm = parsed_request_dict["psm"] + if not runner.thrift_client: + runner.thrift_client = parsed_request_dict["thrift_client"] + if not runner.thrift_client: + ensure_thrift_ready() + from httprunner.thrift.thrift_client import ThriftClient + + runner.thrift_client = ThriftClient( + thrift_file=parsed_request_dict["idl_path"], + service_name=parsed_request_dict["service_name"], + ip=parsed_request_dict["ip"], + port=parsed_request_dict["port"], + include_dirs=parsed_request_dict["include_dirs"], + timeout=parsed_request_dict["timeout"], + proto_type=parsed_request_dict["proto_type"], + trans_type=parsed_request_dict["trans_port"], + ) + + # setup hooks + if step.setup_hooks: + call_hooks(runner, step.setup_hooks, step_variables, "setup request") + + # log request + thrift_request_print = "====== thrift request details ======\n" + thrift_request_print += f"psm: {psm}\n" + for k, v in parsed_request_dict.items(): + v = utils.omit_long_data(v) + thrift_request_print += f"{k}: {repr(v)}\n" + thrift_request_print += "\n" + if ALLURE is not None: + ALLURE.attach( + thrift_request_print, + name="thrift request details", + attachment_type=ALLURE.attachment_type.TEXT, + ) + + # thrift request + resp = runner.thrift_client.send_request( + parsed_request_dict["params"], parsed_request_dict["method"] + ) + resp_obj = ThriftResponseObject(resp, parser=runner.parser) + step_variables["thrift_response"] = resp_obj + + # log response + thrift_response_print = "====== thrift response details ======\n" + for k, v in resp.items(): + v = utils.omit_long_data(v) + thrift_response_print += f"{k}: {repr(v)}\n" + if ALLURE is not None: + ALLURE.attach( + thrift_request_print, + name="thrift response details", + attachment_type=ALLURE.attachment_type.TEXT, + ) + + # teardown hooks + if step.teardown_hooks: + call_hooks(runner, step.teardown_hooks, step_variables, "teardown request") + + def log_thrift_req_resp_details(): + err_msg = "\n{} THRIFT DETAILED REQUEST & RESPONSE {}\n".format( + "*" * 32, "*" * 32 + ) + err_msg += thrift_request_print + thrift_response_print + logger.error(err_msg) + + # extract + extractors = step.extract + extract_mapping = resp_obj.extract(extractors) + step_result.export_vars = extract_mapping + + variables_mapping = step_variables + variables_mapping.update(extract_mapping) + + # validate + validators = step.validators + try: + resp_obj.validate(validators, variables_mapping) + step_result.success = True + except ValidationFailure: + log_thrift_req_resp_details() + raise + finally: + session_data = runner.session.data + session_data.success = step_result.success + session_data.validators = resp_obj.validation_results + + # save step data + step_result.data = session_data + step_result.elapsed = time.time() - start_time + return step_result + + +class StepThriftRequestValidation(StepRequestValidation): + def __init__(self, step: TStep): + self.__step = step + super().__init__(step) + + def run(self, runner: HttpRunner): + return run_step_thrift_request(runner, self.__step) + + +class StepThriftRequestExtraction(StepRequestExtraction): + def __init__(self, step: TStep): + self.__step = step + super().__init__(step) + + def run(self, runner: HttpRunner): + return run_step_thrift_request(runner, self.__step) + + def validate(self) -> StepThriftRequestValidation: + return StepThriftRequestValidation(self.__step) + + +class RunThriftRequest(IStep): + def __init__(self, name: Text): + self.__step = TStep(name=name) + self.__step.thrift_request = TThriftRequest() + + def with_variables(self, **variables) -> "RunThriftRequest": + self.__step.variables.update(variables) + return self + + def with_retry(self, retry_times, retry_interval) -> "RunThriftRequest": + self.__step.retry_times = retry_times + self.__step.retry_interval = retry_interval + return self + + def teardown_hook( + self, hook: Text, assign_var_name: Text = None + ) -> "RunThriftRequest": + if assign_var_name: + self.__step.teardown_hooks.append({assign_var_name: hook}) + else: + self.__step.teardown_hooks.append(hook) + + return self + + def setup_hook( + self, hook: Text, assign_var_name: Text = None + ) -> "RunThriftRequest": + if assign_var_name: + self.__step.setup_hooks.append({assign_var_name: hook}) + else: + self.__step.setup_hooks.append(hook) + + return self + + def with_params(self, **params) -> "RunThriftRequest": + self.__step.thrift_request.params.update(params) + return self + + def with_method(self, method) -> "RunThriftRequest": + self.__step.thrift_request.method = method + return self + + def with_idl_path(self, idl_path, idl_root_path) -> "RunThriftRequest": + self.__step.thrift_request.idl_path = idl_path + self.__step.thrift_request.include_dirs = [idl_root_path] + return self + + def with_thrift_client( + self, thrift_client: Union["ThriftClient", str] + ) -> "RunThriftRequest": + self.__step.thrift_request.thrift_client = thrift_client + return self + + def with_ip(self, ip: str) -> "RunThriftRequest": + self.__step.thrift_request.ip = ip + return self + + def with_port(self, port: int) -> "RunThriftRequest": + self.__step.thrift_request.port = port + return self + + def with_proto_type(self, proto_type: ProtoType) -> "RunThriftRequest": + self.__step.thrift_request.proto_type = proto_type + return self + + def with_trans_type(self, trans_type: TransType) -> "RunThriftRequest": + self.__step.thrift_request.proto_type = trans_type + return self + + def struct(self) -> TStep: + return self.__step + + def name(self) -> Text: + return self.__step.name + + def type(self) -> Text: + return f"thrift-request-{self.__step.thrift_request.psm}-{self.__step.thrift_request.method}" + + def run(self, runner) -> StepResult: + return run_step_thrift_request(runner, self.__step) + + def extract(self) -> StepThriftRequestExtraction: + return StepThriftRequestExtraction(self.__step) + + def validate(self) -> StepThriftRequestValidation: + return StepThriftRequestValidation(self.__step) + + def with_jmespath( + self, jmes_path: Text, var_name: Text + ) -> "StepThriftRequestExtraction": + self.__step.extract[var_name] = jmes_path + return StepThriftRequestExtraction(self.__step) diff --git a/httprunner/thrift/data_convertor.py b/httprunner/thrift/data_convertor.py new file mode 100644 index 0000000..0561ef4 --- /dev/null +++ b/httprunner/thrift/data_convertor.py @@ -0,0 +1,471 @@ +# -*- coding: utf-8 -*- + +from __future__ import division + +import json +import traceback +import re +import logging +import base64 + +from thrift.Thrift import TType + +try: + from _json import encode_basestring_ascii as c_encode_basestring_ascii +except ImportError: + c_encode_basestring_ascii = None + +text_characters = "".join(map(chr, range(32, 127))) + "\n\r\t\b" +_null_trans = str.maketrans("", "") +ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') +ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') +HAS_UTF8 = re.compile(r"[\x80-\xff]") +ESCAPE_DCT = { + "\\": "\\\\", + '"': '\\"', + "\b": "\\b", + "\f": "\\f", + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", +} +for i in range(0x20): + ESCAPE_DCT.setdefault(chr(i), "\\u{0:04x}".format(i)) + # ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) + +INFINITY = float("inf") +FLOAT_REPR = repr + + +def istext(s_input): + """ + 既然我们要判断这串内容是不是可以做为Json的value,那为什么不放下试试呢? + :param s_input: + :return: + """ + return not isinstance(s_input, bytes) + + +def unicode_2_utf8_keep_native(para): + # if type(para) is str: + # return ''.join(filter(lambda x: not str.isalpha(x), para)) + if type(para) is str: + return para + + if type(para) is list: + for i in range(len(para)): + para[i] = unicode_2_utf8_keep_native(para[i]) + return para + elif type(para) is dict: + newpara = {} + for (key, value) in para.items(): + key = unicode_2_utf8_keep_native(key) + value = unicode_2_utf8_keep_native(value) + newpara[key] = value + return newpara + elif type(para) is tuple: + return tuple(unicode_2_utf8_keep_native(list(para))) + elif type(para) is str: + return para.encode("utf-8") + else: + logging.debug("type========", type(para)) + # if issubclass(type(para), dict): + if isinstance(para, dict): + logging.debug("type ************in dict: %s" % (type(para))) + return unicode_2_utf8_keep_native(dict(para)) + else: + return para + + +def encode_basestring(s): + """Return a JSON representation of a Python string""" + + def replace(match): + return ESCAPE_DCT[match.group(0)] + + return '"' + ESCAPE.sub(replace, s) + '"' + + +def py_encode_basestring_ascii(s): + """Return an ASCII-only JSON representation of a Python string""" + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode("utf-8") + + def replace(match): + s = match.group(0) + try: + return ESCAPE_DCT[s] + except KeyError: + n = ord(s) + if n < 0x10000: + return "\\u{0:04x}".format(n) + # return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xD800 | ((n >> 10) & 0x3FF) + s2 = 0xDC00 | (n & 0x3FF) + return "\\u{0:04x}\\u{1:04x}".format(s1, s2) + # return '\\u%04x\\u%04x' % (s1, s2) + + return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' + + +encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii + + +class ThriftJSONDecoder(json.JSONDecoder): + def __init__(self, *args, **kwargs): + self._thrift_class = kwargs.pop("thrift_class") + super(ThriftJSONDecoder, self).__init__(*args, **kwargs) + + def decode(self, json_str): + if isinstance(json_str, dict): + dct = json_str + else: + dct = super(ThriftJSONDecoder, self).decode(json_str) + return self._convert( + dct, + TType.STRUCT, + # (self._thrift_class, self._thrift_class.thrift_spec)) + self._thrift_class, + ) + + def _convert(self, val, ttype, ttype_info): + if ttype == TType.STRUCT: + if val is None: + ret = None + else: + # (thrift_class, thrift_spec) = ttype_info + thrift_class = ttype_info + thrift_spec = ttype_info.thrift_spec + ret = thrift_class() + for tag, field in thrift_spec.items(): + if field is None: + continue + # {1: (15, 'ad_ids', 10, False), 255: (12, 'Base', , False)} + # {1: (15, 'models', (12, ), False), 255: (12, 'BaseResp', , False)} + if len(field) <= 3: + (field_ttype, field_name, dummy) = field + field_ttype_info = None + else: + (field_ttype, field_name, field_ttype_info, dummy) = field + + if val is None or field_name not in val: + continue + converted_val = self._convert( + val[field_name], field_ttype, field_ttype_info + ) + setattr(ret, field_name, converted_val) + elif ttype == TType.LIST: + if type(ttype_info) != tuple: # 说明是基础类型了, 无法在细分 + (element_ttype, element_ttype_info) = (ttype_info, None) + else: + (element_ttype, element_ttype_info) = ttype_info + if val is not None: + ret = [self._convert(x, element_ttype, element_ttype_info) for x in val] + else: + ret = None + + elif ttype == TType.SET: + if type(ttype_info) != tuple: # 说明是基础类型了, 无法在细分 + (element_ttype, element_ttype_info) = (ttype_info, None) + else: + (element_ttype, element_ttype_info) = ttype_info + if val is not None: + ret = set( + [self._convert(x, element_ttype, element_ttype_info) for x in val] + ) + else: + ret = None + + elif ttype == TType.MAP: + # key处理 + if type(ttype_info[0]) == tuple: + key_ttype, key_ttype_info = ttype_info[0] + else: + key_ttype, key_ttype_info = ttype_info[0], None + + # value处理 + if type(ttype_info[1]) != tuple: # 说明value为基础类型, 已不可在细分 + val_ttype = ttype_info[1] + val_ttype_info = None + else: + val_ttype, val_ttype_info = ttype_info[1] + + if val is not None: + ret = dict( + [ + ( + self._convert(k, key_ttype, key_ttype_info), + self._convert(v, val_ttype, val_ttype_info), + ) + for (k, v) in val.items() + ] + ) + else: + ret = None + elif ttype == TType.STRING: + if isinstance(val, str): + ret = val.encode("utf8") + elif val is None: + ret = None + else: + ret = str(val) + # 判断string字段是否是base64编码后的string, 如果是则此处需要对该string字段进行b64decode, 还原成原本的字符串 + # todo : 留待实现 + + elif ttype == TType.DOUBLE: + if val is not None: + ret = float(val) + else: + ret = None + elif ttype == TType.I64: + if val is not None: + ret = int(val) + else: + ret = None + elif ttype == TType.I32 or ttype == TType.I16 or ttype == TType.BYTE: + if val is not None: + ret = int(val) + else: + ret = None + elif ttype == TType.BOOL: + if val is not None: + ret = bool(val) + else: + ret = None + else: + raise TypeError("Unrecognized thrift field type: %s" % ttype) + return ret + + +def json2thrift(json_str, thrift_class): + logging.debug(json_str) + return json.loads( + json_str, cls=ThriftJSONDecoder, thrift_class=thrift_class, strict=False + ) + + +def dumper(obj): + try: + return json.dumps(obj, default=lambda o: o.__dict__, sort_keys=True, indent=2) + except: + return obj.__dict__ + + +class MyJSONEncoder(json.JSONEncoder): + def __init__( + self, + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding="utf-8", + default=None, + sort_keys=False, + **kw + ): + super(MyJSONEncoder, self).__init__( + skipkeys=skipkeys, + ensure_ascii=ensure_ascii, + check_circular=check_circular, + allow_nan=allow_nan, + indent=indent, + separators=separators, + encoding=encoding, + default=default, + sort_keys=sort_keys, + ) + self.skip_nonutf8_value = kw.get( + "skip_nonutf8_value", False + ) # 默认不skip忽略非utf-8编码的字段 + + def encode(self, o): + """Return a JSON string representation of a Python data structure. + JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + + """ + # This is for extremely simple cases and benchmarks. + + if isinstance(o, str): + + if isinstance(o, str): + _encoding = self.encoding + if _encoding is not None and not (_encoding == "utf-8"): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + chunks = self.iterencode(o, _one_shot=True) + if not isinstance(chunks, (list, tuple)): + chunks = list(chunks) + # add by braver + # todo: fix 'utf8' codec can't decode byte 0x91 in position 3: invalid start byte" + if self.skip_nonutf8_value: # 缺省为false + tmp_chunks = [] + for chunk in chunks: + try: + tmp_chunks.append(unicode_2_utf8_keep_native(chunk)) + except Exception as err: + logging.debug(traceback.format_exc()) + return "".join(tmp_chunks) + + # 保留老的逻辑, /usr/lib/python2.7/package/json/__init__.py dumps接口 + return "".join(chunks) + + +class ThriftJSONEncoder(json.JSONEncoder): + """ + add by braver + """ + + def __init__( + self, + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + default=None, + sort_keys=False, + **kw + ): + + super(ThriftJSONEncoder, self).__init__( + skipkeys=skipkeys, + ensure_ascii=ensure_ascii, + check_circular=check_circular, + allow_nan=allow_nan, + indent=indent, + separators=separators, + default=default, + sort_keys=sort_keys, + ) + self.skip_nonutf8_value = kw.get( + "skip_nonutf8_value", False + ) # 默认不skip忽略非utf-8编码的字段 + + def encode(self, o): + """Return a JSON string representation of a Python data structure. + JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + + """ + # This is for extremely simple cases and benchmarks. + + if isinstance(o, str): + if isinstance(o, str): + _encoding = self.encoding + if _encoding is not None and not (_encoding == "utf-8"): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + chunks = self.iterencode(o, _one_shot=True) + if not isinstance(chunks, (list, tuple)): + chunks = list(chunks) + # add by braver + # todo: fix 'utf8' codec can't decode byte 0x91 in position 3: invalid start byte" + if self.skip_nonutf8_value: # 缺省为false + tmp_chunks = [] + for chunk in chunks: + try: + tmp_chunks.append(unicode_2_utf8_keep_native(chunk)) + except Exception as err: + logging.debug(traceback.format_exc()) + return "".join(tmp_chunks) + + # 保留老的逻辑, /usr/lib/python2.7/package/json/__init__.py dumps接口 + return "".join(chunks) + + def default(self, o): + if isinstance(o, bytes): + return str(o, encoding="utf-8") + if not hasattr(o, "thrift_spec"): + return super(ThriftJSONEncoder, self).default(o) + + spec = getattr(o, "thrift_spec") + ret = {} + for tag, field in spec.items(): + if field is None: + continue + # (tag, field_ttype, field_name, field_ttype_info, default) = field + field_name = field[1] + default = field[-1] + field_type = field[0] + field_ttype_info = field[2] + # if field_type in [TType.STRING, TType.BINARY]: # 说明是string(明文string或者binary) + # if field_type in [TType.STRING, TType.BYTE]: # 说明是string(明文string或者binary) + if field_name in o.__dict__: + val = o.__dict__[field_name] + if field_type in [TType.LIST, TType.SET]: # 数组类型 + if val: # val为非空数组/Set + val = list(val) # 统一转成数组(list/set) + is_need_binary_bs64 = False + if type(field_ttype_info) != tuple: # 基础类型 + if ( + field_ttype_info in [TType.BYTE] + and type(val[0]) in [str] + and not istext(val[0]) + ): + is_need_binary_bs64 = True + if is_need_binary_bs64: + for index, item in enumerate(val): + if item and type(item) in [str] and not istext(item): + val[index] = base64.b64encode( + item + ) # 判断为二进制字符串, 需要进行base64编码 + if field_type in [TType.BYTE] and type(val) in [ + str + ]: # 说明是string(明文string或者binary) + # 需要对二进制字节字符串字段进行base64编码, 将二进制字节串字段->ascii字符编码的base64编码明文串 + if val and not istext(val): # 说明是该字段非空且为binary string + print("4" * 100, val) + val = base64.b64encode(val.encode("utf-8")) + # val = base64.b64encode(val) # 进行base64编码处理, 不然该字段序列化为json时会报错 + # if val != default: + ret[field_name] = val + if "request_id" in o.__dict__: + ret["request_id"] = o.__dict__["request_id"] + if "rpc_latency" in o.__dict__: + ret["rpc_latency"] = o.__dict__["rpc_latency"] + return ret + + +def thrift2json(obj, skip_nonutf8_value=False): + return json.dumps( + obj, + cls=ThriftJSONEncoder, + ensure_ascii=False, + skip_nonutf8_value=skip_nonutf8_value, + ) + + +def thrift2dict(obj): + str = thrift2json(obj) + return json.loads(str) + + +dict2thrift = json2thrift + +if __name__ == "__main__": + print(istext("Всего за {$price$}, а доставка - бесплатно!")) + print(istext(b"\xe4\xb8\xad\xe6\x96\x87")) + print( + istext( + '{"web_uri":"ad-site-i18n-sg/202103185d0d723d88b7f642452dac73","height":336,"width":336,"file_name":""}' + ) + ) diff --git a/httprunner/thrift/thrift_client.py b/httprunner/thrift/thrift_client.py new file mode 100644 index 0000000..8d5a339 --- /dev/null +++ b/httprunner/thrift/thrift_client.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import + +import enum +import json + +import thriftpy2 +from loguru import logger +from thriftpy2.protocol import ( + TBinaryProtocolFactory, + TCompactProtocolFactory, + TCyBinaryProtocolFactory, + TJSONProtocolFactory, +) +from thriftpy2.rpc import make_client +from thriftpy2.transport import ( + TBufferedTransportFactory, + TCyBufferedTransportFactory, + TCyFramedTransportFactory, + TFramedTransportFactory, +) + +from httprunner.thrift.data_convertor import json2thrift, thrift2dict + + +class ProtoType(enum.Enum): + Binary = 1 + CyBinary = 2 + Compact = 3 + Json = 4 + + +class TransType(enum.Enum): + Buffered = 1 + CyBuffered = 2 + Framed = 3 + CyFramed = 4 + + +class RequestFormat(enum.Enum): + json = 1 + binary = 2 + + +def get_proto_factory(proto_type): + if proto_type == ProtoType.Binary: + return TBinaryProtocolFactory() + if proto_type == ProtoType.CyBinary: + return TCyBinaryProtocolFactory() + if proto_type == ProtoType.Compact: + return TCompactProtocolFactory() + if proto_type == ProtoType.Json: + return TJSONProtocolFactory() + + +def get_trans_factory(trans_type): + if trans_type == TransType.Buffered: + return TBufferedTransportFactory() + if trans_type == TransType.CyBuffered: + return TCyBufferedTransportFactory() + if trans_type == TransType.Framed: + return TFramedTransportFactory() + if trans_type == TransType.CyFramed: + return TCyFramedTransportFactory() + + +class ThriftClient(object): + def __init__( + self, + thrift_file, + service_name, + ip, + port, + include_dirs=None, + timeout=3000, + proto_type=ProtoType.CyBinary, + trans_type=TransType.CyBuffered, + ): + self.thrift_file = thrift_file + self.include_dirs = include_dirs + self.service_name = service_name + self.ip = ip + self.port = port + self.timeout = timeout + self.proto_type = proto_type + self.trans_type = trans_type + try: + logger.debug( + "init thrift module: thrift_file=%s, module_name=%s", + thrift_file, + str(self.service_name) + "_thrift", + ) + self.thrift_module = thriftpy2.load( + self.thrift_file, + module_name=str(self.service_name) + "_thrift", + include_dirs=self.include_dirs, + ) + self.thrift_service_obj = getattr(self.thrift_module, self.service_name) + logger.debug( + "init thrift client: service_name=%s, ip=%s, port=%s", + self.thrift_service_obj, + ip, + port, + ) + self.client = make_client( + self.thrift_service_obj, + self.ip, + int(self.port), + timeout=self.timeout, + proto_factory=get_proto_factory(self.proto_type), + trans_factory=get_trans_factory(self.trans_type), + ) + except Exception as e: + self.thrift_module = None + self.thrift_service_obj = None + self.client = None + logger.exception("init thrift module and client failed: {}".format(e)) + finally: + thriftpy2.parser.parser.thrift_stack = [] + + def get_client(self): + return self.client + + def send_request(self, request_data, request_method=""): + thrift_req_cls = getattr( + self.thrift_service_obj, request_method + "_args" + ).thrift_spec[1][2] + request_obj = json2thrift(json.dumps(request_data), thrift_req_cls) + logger.debug( + "send thrift request: request_method=%s, request_obj=%s", + request_method, + request_obj, + ) + response_obj = getattr(self.client, request_method)(request_obj) + logger.debug("thrift response = %s", response_obj) + return thrift2dict(response_obj) + + def __del__(self): + self.client.close() diff --git a/httprunner/utils.py b/httprunner/utils.py new file mode 100644 index 0000000..b189832 --- /dev/null +++ b/httprunner/utils.py @@ -0,0 +1,366 @@ +import collections +import copy +import itertools +import json +import os +import os.path +import platform +import random +import sys +import time +import uuid +from multiprocessing import Queue +from typing import Any, Dict, List + +import requests +import sentry_sdk +from loguru import logger + +from httprunner import __version__, exceptions +from httprunner.models import VariablesMapping + + +""" run httpbin as test service +https://github.com/postmanlabs/httpbin + +$ docker pull kennethreitz/httpbin +$ docker run -p 80:80 kennethreitz/httpbin +""" +HTTP_BIN_URL = "http://127.0.0.1:80" + + +def get_platform(): + return { + "httprunner_version": __version__, + "python_version": "{} {}".format( + platform.python_implementation(), platform.python_version() + ), + "platform": platform.platform(), + } + + +def init_sentry_sdk(): + if os.getenv("DISABLE_SENTRY") == "true": + return + + sentry_sdk.init( + dsn="https://460e31339bcb428c879aafa6a2e78098@sentry.io/5263855", + release="httprunner@{}".format(__version__), + ) + with sentry_sdk.configure_scope() as scope: + scope.set_user({"id": uuid.getnode()}) + + +class GA4Client(object): + """send events to Google Analytics 4 via Measurement Protocol. + get details in hrp/internal/sdk/ga4.go + """ + + def __init__( + self, measurement_id: str, api_secret: str, debug: bool = False + ) -> None: + self.http_client = requests.Session() + + self.debug = debug + if debug: + uri = "https://www.google-analytics.com/debug/mp/collect" + else: + uri = "https://www.google-analytics.com/mp/collect" + + self.uri = f"{uri}?measurement_id={measurement_id}&api_secret={api_secret}" + self.user_id = str(uuid.getnode()) + self.common_event_params = get_platform() + + # do not send GA events in CI environment + self.__is_ci = os.getenv("DISABLE_GA") == "true" + + def send_event(self, name: str, event_params: dict = None) -> None: + if self.__is_ci: + return + + event_params = event_params or {} + event_params.update(self.common_event_params) + event = { + "name": name, + "params": event_params, + } + + payload = { + "client_id": f"{int(random.random() * 10**8)}.{int(time.time())}", + "user_id": self.user_id, + "timestamp_micros": int(time.time() * 10**6), + "events": [event], + } + + if self.debug: + logger.debug(f"send GA4 event, uri: {self.uri}, payload: {payload}") + + try: + resp = self.http_client.post(self.uri, json=payload, timeout=5) + except Exception as err: # ProxyError, SSLError, ConnectionError + logger.error(f"request GA4 failed, error: {err}") + return + + if resp.status_code >= 300: + logger.error( + f"validation response got unexpected status: {resp.status_code}" + ) + return + + if not self.debug: + return + + try: + resp_body = resp.json() + logger.debug( + "get GA4 validation response, " + f"status code: {resp.status_code}, body: {resp_body}" + ) + except Exception: + pass + + +GA4_MEASUREMENT_ID = "G-9KHR3VC2LN" +GA4_API_SECRET = "w7lKNQIrQsKNS4ikgMPp0Q" + +ga4_client = GA4Client(GA4_MEASUREMENT_ID, GA4_API_SECRET, False) + + +def set_os_environ(variables_mapping): + """set variables mapping to os.environ""" + for variable in variables_mapping: + os.environ[variable] = variables_mapping[variable] + logger.debug(f"Set OS environment variable: {variable}") + + +def unset_os_environ(variables_mapping): + """unset variables mapping to os.environ""" + for variable in variables_mapping: + os.environ.pop(variable) + logger.debug(f"Unset OS environment variable: {variable}") + + +def get_os_environ(variable_name): + """get value of environment variable. + + Args: + variable_name(str): variable name + + Returns: + value of environment variable. + + Raises: + exceptions.EnvNotFound: If environment variable not found. + + """ + try: + return os.environ[variable_name] + except KeyError: + raise exceptions.EnvNotFound(variable_name) + + +def lower_dict_keys(origin_dict): + """convert keys in dict to lower case + + Args: + origin_dict (dict): mapping data structure + + Returns: + dict: mapping with all keys lowered. + + Examples: + >>> origin_dict = { + "Name": "", + "Request": "", + "URL": "", + "METHOD": "", + "Headers": "", + "Data": "" + } + >>> lower_dict_keys(origin_dict) + { + "name": "", + "request": "", + "url": "", + "method": "", + "headers": "", + "data": "" + } + + """ + if not origin_dict or not isinstance(origin_dict, dict): + return origin_dict + + return {key.lower(): value for key, value in origin_dict.items()} + + +def print_info(info_mapping): + """print info in mapping. + + Args: + info_mapping (dict): input(variables) or output mapping. + + Examples: + >>> info_mapping = { + "var_a": "hello", + "var_b": "world" + } + >>> info_mapping = { + "status_code": 500 + } + >>> print_info(info_mapping) + ==================== Output ==================== + Key : Value + ---------------- : ---------------------------- + var_a : hello + var_b : world + ------------------------------------------------ + + """ + if not info_mapping: + return + + content_format = "{:<16} : {:<}\n" + content = "\n==================== Output ====================\n" + content += content_format.format("Variable", "Value") + content += content_format.format("-" * 16, "-" * 29) + + for key, value in info_mapping.items(): + if isinstance(value, (tuple, collections.deque)): + continue + elif isinstance(value, (dict, list)): + value = json.dumps(value) + elif value is None: + value = "None" + + content += content_format.format(key, value) + + content += "-" * 48 + "\n" + logger.info(content) + + +def omit_long_data(body, omit_len=512): + """omit too long str/bytes""" + if not isinstance(body, (str, bytes)): + return body + + body_len = len(body) + if body_len <= omit_len: + return body + + omitted_body = body[0:omit_len] + + appendix_str = f" ... OMITTED {body_len - omit_len} CHARACTORS ..." + if isinstance(body, bytes): + appendix_str = appendix_str.encode("utf-8") + + return omitted_body + appendix_str + + +def sort_dict_by_custom_order(raw_dict: Dict, custom_order: List): + def get_index_from_list(lst: List, item: Any): + try: + return lst.index(item) + except ValueError: + # item is not in lst + return len(lst) + 1 + + return dict( + sorted(raw_dict.items(), key=lambda i: get_index_from_list(custom_order, i[0])) + ) + + +class ExtendJSONEncoder(json.JSONEncoder): + """especially used to safely dump json data with python object, + such as MultipartEncoder""" + + def default(self, obj): + try: + return super(ExtendJSONEncoder, self).default(obj) + except (UnicodeDecodeError, TypeError): + return repr(obj) + + +def merge_variables( + variables: VariablesMapping, variables_to_be_overridden: VariablesMapping +) -> VariablesMapping: + """merge two variables mapping, the first variables have higher priority""" + step_new_variables = {} + for key, value in variables.items(): + if f"${key}" == value or "${" + key + "}" == value: + # e.g. {"base_url": "$base_url"} + # or {"base_url": "${base_url}"} + continue + + step_new_variables[key] = value + + merged_variables = copy.copy(variables_to_be_overridden) + merged_variables.update(step_new_variables) + return merged_variables + + +def is_support_multiprocessing() -> bool: + try: + Queue() + return True + except (ImportError, OSError): + # system that does not support semaphores + # (dependency of multiprocessing), like Android termux + return False + + +def gen_cartesian_product(*args: List[Dict]) -> List[Dict]: + """generate cartesian product for lists + + Args: + args (list of list): lists to be generated with cartesian product + + Returns: + list: cartesian product in list + + Examples: + + >>> arg1 = [{"a": 1}, {"a": 2}] + >>> arg2 = [{"x": 111, "y": 112}, {"x": 121, "y": 122}] + >>> args = [arg1, arg2] + >>> gen_cartesian_product(*args) + >>> # same as below + >>> gen_cartesian_product(arg1, arg2) + [ + {'a': 1, 'x': 111, 'y': 112}, + {'a': 1, 'x': 121, 'y': 122}, + {'a': 2, 'x': 111, 'y': 112}, + {'a': 2, 'x': 121, 'y': 122} + ] + + """ + if not args: + return [] + elif len(args) == 1: + return args[0] + + product_list = [] + for product_item_tuple in itertools.product(*args): + product_item_dict = {} + for item in product_item_tuple: + product_item_dict.update(item) + + product_list.append(product_item_dict) + + return product_list + + +LOGGER_FORMAT = ( + "{time:YYYY-MM-DD HH:mm:ss.SSS}" + + " | {level} | {message}" +) + + +def init_logger(level: str): + level = level.upper() + if level not in ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: + level = "INFO" # default + + # set log level to INFO + logger.remove() + logger.add(sys.stdout, format=LOGGER_FORMAT, level=level) diff --git a/httprunner/utils_test.py b/httprunner/utils_test.py new file mode 100644 index 0000000..47fac6c --- /dev/null +++ b/httprunner/utils_test.py @@ -0,0 +1,171 @@ +import decimal +import json +import os +import unittest +from pathlib import Path + +import toml + +from httprunner import __version__, loader, utils +from httprunner.utils import ExtendJSONEncoder, merge_variables, ga4_client + + +class TestUtils(unittest.TestCase): + def test_set_os_environ(self): + self.assertNotIn("abc", os.environ) + variables_mapping = {"abc": "123"} + utils.set_os_environ(variables_mapping) + self.assertIn("abc", os.environ) + self.assertEqual(os.environ["abc"], "123") + + def test_validators(self): + from httprunner.builtin import comparators + + functions_mapping = loader.load_module_functions(comparators) + + functions_mapping["equal"](None, None) + functions_mapping["equal"](1, 1) + functions_mapping["equal"]("abc", "abc") + with self.assertRaises(AssertionError): + functions_mapping["equal"]("123", 123) + + functions_mapping["less_than"](1, 2) + functions_mapping["less_or_equals"](2, 2) + + functions_mapping["greater_than"](2, 1) + functions_mapping["greater_or_equals"](2, 2) + + functions_mapping["not_equal"](123, "123") + + functions_mapping["length_equal"]("123", 3) + with self.assertRaises(AssertionError): + functions_mapping["length_equal"]("123", "3") + with self.assertRaises(AssertionError): + functions_mapping["length_equal"]("123", "abc") + functions_mapping["length_greater_than"]("123", 2) + functions_mapping["length_greater_or_equals"]("123", 3) + + functions_mapping["contains"]("123abc456", "3ab") + functions_mapping["contains"](["1", "2"], "1") + functions_mapping["contains"]({"a": 1, "b": 2}, "a") + functions_mapping["contained_by"]("3ab", "123abc456") + functions_mapping["contained_by"](0, [0, 200]) + + functions_mapping["regex_match"]("123abc456", "^123\w+456$") + with self.assertRaises(AssertionError): + functions_mapping["regex_match"]("123abc456", "^12b.*456$") + + functions_mapping["startswith"]("abc123", "ab") + functions_mapping["startswith"]("123abc", 12) + functions_mapping["startswith"](12345, 123) + + functions_mapping["endswith"]("abc123", 23) + functions_mapping["endswith"]("123abc", "abc") + functions_mapping["endswith"](12345, 45) + + functions_mapping["type_match"](580509390, int) + functions_mapping["type_match"](580509390, "int") + functions_mapping["type_match"]([], list) + functions_mapping["type_match"]([], "list") + functions_mapping["type_match"]([1], "list") + functions_mapping["type_match"]({}, "dict") + functions_mapping["type_match"]({"a": 1}, "dict") + functions_mapping["type_match"](None, "None") + functions_mapping["type_match"](None, "NoneType") + functions_mapping["type_match"](None, None) + + def test_lower_dict_keys(self): + request_dict = { + "url": "http://127.0.0.1:5000", + "METHOD": "POST", + "Headers": {"Accept": "application/json", "User-Agent": "ios/9.3"}, + } + new_request_dict = utils.lower_dict_keys(request_dict) + self.assertIn("method", new_request_dict) + self.assertIn("headers", new_request_dict) + self.assertIn("Accept", new_request_dict["headers"]) + self.assertIn("User-Agent", new_request_dict["headers"]) + + request_dict = "$default_request" + new_request_dict = utils.lower_dict_keys(request_dict) + self.assertEqual("$default_request", request_dict) + + request_dict = None + new_request_dict = utils.lower_dict_keys(request_dict) + self.assertEqual(None, request_dict) + + def test_print_info(self): + info_mapping = {"a": 1, "t": (1, 2), "b": {"b1": 123}, "c": None, "d": [4, 5]} + utils.print_info(info_mapping) + + def test_sort_dict_by_custom_order(self): + self.assertEqual( + list( + utils.sort_dict_by_custom_order( + {"C": 3, "D": 2, "A": 1, "B": 8}, ["A", "D"] + ).keys() + ), + ["A", "D", "C", "B"], + ) + + def test_safe_dump_json(self): + class A(object): + pass + + data = {"a": A(), "b": decimal.Decimal("1.45")} + + with self.assertRaises(TypeError): + json.dumps(data) + + json.dumps(data, cls=ExtendJSONEncoder) + + def test_override_config_variables(self): + step_variables = {"base_url": "$base_url", "foo1": "bar1"} + config_variables = {"base_url": "https://postman-echo.com", "foo1": "bar111"} + self.assertEqual( + merge_variables(step_variables, config_variables), + {"base_url": "https://postman-echo.com", "foo1": "bar1"}, + ) + + def test_cartesian_product_one(self): + parameters_content_list = [[{"a": 1}, {"a": 2}]] + product_list = utils.gen_cartesian_product(*parameters_content_list) + self.assertEqual(product_list, [{"a": 1}, {"a": 2}]) + + def test_cartesian_product_multiple(self): + parameters_content_list = [ + [{"a": 1}, {"a": 2}], + [{"x": 111, "y": 112}, {"x": 121, "y": 122}], + ] + product_list = utils.gen_cartesian_product(*parameters_content_list) + self.assertEqual( + product_list, + [ + {"a": 1, "x": 111, "y": 112}, + {"a": 1, "x": 121, "y": 122}, + {"a": 2, "x": 111, "y": 112}, + {"a": 2, "x": 121, "y": 122}, + ], + ) + + def test_cartesian_product_empty(self): + parameters_content_list = [] + product_list = utils.gen_cartesian_product(*parameters_content_list) + self.assertEqual(product_list, []) + + def test_versions_are_in_sync(self): + """Checks if the pyproject.toml and __version__ in __init__.py are in sync.""" + + path = Path(__file__).resolve().parents[1] / "pyproject.toml" + pyproject = toml.loads(open(str(path)).read()) + pyproject_version = pyproject["tool"]["poetry"]["version"] + self.assertEqual(pyproject_version, __version__) + + def test_ga4_send_event(self): + ga4_client.send_event( + "httprunner_debug_event", + { + "a": 123, + "b": 456, + }, + ) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..5dc4bf4 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1447 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "allure-pytest" +version = "2.13.2" +description = "Allure pytest integration" +optional = true +python-versions = "*" +files = [ + {file = "allure-pytest-2.13.2.tar.gz", hash = "sha256:22243159e8ec81ce2b5254b4013802198821b1b42f118f69d4a289396607c7b3"}, + {file = "allure_pytest-2.13.2-py3-none-any.whl", hash = "sha256:17de9dbee7f61c8e66a5b5e818b00e419dbcea44cb55c24319401ba813220690"}, +] + +[package.dependencies] +allure-python-commons = "2.13.2" +pytest = ">=4.5.0" + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "allure-python-commons" +version = "2.13.2" +description = "Common module for integrate allure with python-based frameworks" +optional = true +python-versions = ">=3.6" +files = [ + {file = "allure-python-commons-2.13.2.tar.gz", hash = "sha256:8a03681330231b1deadd86b97ff68841c6591320114ae638570f1ed60d7a2033"}, + {file = "allure_python_commons-2.13.2-py3-none-any.whl", hash = "sha256:2bb3646ec3fbf5b36d178a5e735002bc130ae9f9ba80f080af97d368ba375051"}, +] + +[package.dependencies] +attrs = ">=16.0.0" +pluggy = ">=0.4.0" + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +optional = true +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "brotli" +version = "1.0.9" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +files = [ + {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"}, + {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"}, + {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"}, + {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"}, + {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"}, + {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"}, + {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"}, + {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"}, + {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"}, + {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"}, + {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"}, + {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"}, + {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"}, + {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"}, + {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"}, + {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"}, + {file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cc0283a406774f465fb45ec7efb66857c09ffefbe49ec20b7882eff6d3c86d3a"}, + {file = "Brotli-1.0.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:11d3283d89af7033236fa4e73ec2cbe743d4f6a81d41bd234f24bf63dde979df"}, + {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1306004d49b84bd0c4f90457c6f57ad109f5cc6067a9664e12b7b79a9948ad"}, + {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1375b5d17d6145c798661b67e4ae9d5496920d9265e2f00f1c2c0b5ae91fbde"}, + {file = "Brotli-1.0.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cab1b5964b39607a66adbba01f1c12df2e55ac36c81ec6ed44f2fca44178bf1a"}, + {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ed6a5b3d23ecc00ea02e1ed8e0ff9a08f4fc87a1f58a2530e71c0f48adf882f"}, + {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cb02ed34557afde2d2da68194d12f5719ee96cfb2eacc886352cb73e3808fc5d"}, + {file = "Brotli-1.0.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b3523f51818e8f16599613edddb1ff924eeb4b53ab7e7197f85cbc321cdca32f"}, + {file = "Brotli-1.0.9-cp311-cp311-win32.whl", hash = "sha256:ba72d37e2a924717990f4d7482e8ac88e2ef43fb95491eb6e0d124d77d2a150d"}, + {file = "Brotli-1.0.9-cp311-cp311-win_amd64.whl", hash = "sha256:3ffaadcaeafe9d30a7e4e1e97ad727e4f5610b9fa2f7551998471e3736738679"}, + {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"}, + {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"}, + {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"}, + {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"}, + {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"}, + {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"}, + {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"}, + {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"}, + {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"}, + {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"}, + {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"}, + {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"}, + {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"}, + {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"}, + {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"}, + {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"}, + {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"}, + {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"}, + {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"}, + {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"}, + {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"}, + {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"}, + {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"}, + {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"}, + {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"}, + {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"}, + {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"}, + {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"}, + {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"}, + {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"}, + {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"}, + {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"}, + {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"}, + {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"}, + {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"}, + {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"}, + {file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73fd30d4ce0ea48010564ccee1a26bfe39323fde05cb34b5863455629db61dc7"}, + {file = "Brotli-1.0.9-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02177603aaca36e1fd21b091cb742bb3b305a569e2402f1ca38af471777fb019"}, + {file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"}, + {file = "Brotli-1.0.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b43775532a5904bc938f9c15b77c613cb6ad6fb30990f3b0afaea82797a402d8"}, + {file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5bf37a08493232fbb0f8229f1824b366c2fc1d02d64e7e918af40acd15f3e337"}, + {file = "Brotli-1.0.9-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:330e3f10cd01da535c70d09c4283ba2df5fb78e915bea0a28becad6e2ac010be"}, + {file = "Brotli-1.0.9-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e1abbeef02962596548382e393f56e4c94acd286bd0c5afba756cffc33670e8a"}, + {file = "Brotli-1.0.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3148362937217b7072cf80a2dcc007f09bb5ecb96dae4617316638194113d5be"}, + {file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336b40348269f9b91268378de5ff44dc6fbaa2268194f85177b53463d313842a"}, + {file = "Brotli-1.0.9-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b09a16a1950b9ef495a0f8b9d0a87599a9d1f179e2d4ac014b2ec831f87e7"}, + {file = "Brotli-1.0.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c8e521a0ce7cf690ca84b8cc2272ddaf9d8a50294fd086da67e517439614c755"}, + {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "charset-normalizer" +version = "3.2.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, + {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, + {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, + {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, + {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, + {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, + {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "click" +version = "8.1.6" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, + {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "coverage" +version = "4.5.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" +files = [ + {file = "coverage-4.5.4-cp26-cp26m-macosx_10_12_x86_64.whl", hash = "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28"}, + {file = "coverage-4.5.4-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c"}, + {file = "coverage-4.5.4-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce"}, + {file = "coverage-4.5.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe"}, + {file = "coverage-4.5.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888"}, + {file = "coverage-4.5.4-cp27-cp27m-win32.whl", hash = "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc"}, + {file = "coverage-4.5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24"}, + {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437"}, + {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6"}, + {file = "coverage-4.5.4-cp33-cp33m-macosx_10_10_x86_64.whl", hash = "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5"}, + {file = "coverage-4.5.4-cp34-cp34m-macosx_10_12_x86_64.whl", hash = "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef"}, + {file = "coverage-4.5.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e"}, + {file = "coverage-4.5.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca"}, + {file = "coverage-4.5.4-cp34-cp34m-win32.whl", hash = "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0"}, + {file = "coverage-4.5.4-cp34-cp34m-win_amd64.whl", hash = "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1"}, + {file = "coverage-4.5.4-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7"}, + {file = "coverage-4.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47"}, + {file = "coverage-4.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"}, + {file = "coverage-4.5.4-cp35-cp35m-win32.whl", hash = "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e"}, + {file = "coverage-4.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d"}, + {file = "coverage-4.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9"}, + {file = "coverage-4.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755"}, + {file = "coverage-4.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9"}, + {file = "coverage-4.5.4-cp36-cp36m-win32.whl", hash = "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f"}, + {file = "coverage-4.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5"}, + {file = "coverage-4.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca"}, + {file = "coverage-4.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650"}, + {file = "coverage-4.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2"}, + {file = "coverage-4.5.4-cp37-cp37m-win32.whl", hash = "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5"}, + {file = "coverage-4.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351"}, + {file = "coverage-4.5.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5"}, + {file = "coverage-4.5.4.tar.gz", hash = "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "cython" +version = "0.29.36" +description = "The Cython compiler for writing C extensions for the Python language." +optional = true +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "Cython-0.29.36-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea33c1c57f331f5653baa1313e445fbe80d1da56dd9a42c8611037887897b9d"}, + {file = "Cython-0.29.36-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2fe34615c13ace29e77bf9d21c26188d23eff7ad8b3e248da70404e5f5436b95"}, + {file = "Cython-0.29.36-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ae75eac4f13cbbcb50b2097470dcea570182446a3ebd0f7e95dd425c2017a2d7"}, + {file = "Cython-0.29.36-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:847d07fc02978c4433d01b4f5ee489b75fd42fd32ccf9cc4b5fd887e8cffe822"}, + {file = "Cython-0.29.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7cb44aeaf6c5c25bd6a7562ece4eadf50d606fc9b5f624fa95bd0281e8bf0a97"}, + {file = "Cython-0.29.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:28fb10aabd56a2e4d399273b48e106abe5a0d271728fd5eed3d36e7171000045"}, + {file = "Cython-0.29.36-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:86b7a13c6b23ab6471d40a320f573fbc8a4e39833947eebed96661145dc34771"}, + {file = "Cython-0.29.36-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:19ccf7fc527cf556e2e6a3dfeffcadfbcabd24a59a988289117795dfed8a25ad"}, + {file = "Cython-0.29.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:74bddfc7dc8958526b2018d3adc1aa6dc9cf2a24095c972e5ad06758c360b261"}, + {file = "Cython-0.29.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6c4d7e36fe0211e394adffd296382b435ac22762d14f2fe45c506c230f91cf2d"}, + {file = "Cython-0.29.36-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:0bca6a7504e8cfc63a4d3c7c9b9a04e5d05501942a6c8cee177363b61a32c2d4"}, + {file = "Cython-0.29.36-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17c74f80b06e2fa8ffc8acd41925f4f9922da8a219cd25c6901beab2f7c56cc5"}, + {file = "Cython-0.29.36-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:25ff471a459aad82146973b0b8c177175ab896051080713d3035ad4418739f66"}, + {file = "Cython-0.29.36-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9738f23d00d99481797b155ad58f8fc1c72096926ea2554b8ccc46e1d356c27"}, + {file = "Cython-0.29.36-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:af2f333f08c4c279f3480532341bf70ec8010bcbc7d8a6daa5ca0bf4513af295"}, + {file = "Cython-0.29.36-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:cd77cedbcc13cb67aef39b8615fd50a67fc42b0c6defea6fc0a21e19d3a062ec"}, + {file = "Cython-0.29.36-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50d506d73a46c4a522ef9fdafcbf7a827ba13907b18ff58f61a8fa0887d0bd8d"}, + {file = "Cython-0.29.36-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:6a571d7c7b52ee12d73bc65b4855779c069545da3bac26bec06a1389ad17ade5"}, + {file = "Cython-0.29.36-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a216b2801c7d9c3babe0a10cc25da3bc92494d7047d1f732d3c47b0cceaf0941"}, + {file = "Cython-0.29.36-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:68abee3be27f21c9642a07a93f8333d491f4c52bc70068e42f51685df9ac1a57"}, + {file = "Cython-0.29.36-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:1ef90023da8a9bf84cf16f06186db0906d2ce52a09f751e2cb9d3da9d54eae46"}, + {file = "Cython-0.29.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9deef0761e8c798043dbb728a1c6df97b26e5edc65b8d6c7608b3c07af3eb722"}, + {file = "Cython-0.29.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:69af2365de2343b4e5a61c567e7611ddf2575ae6f6e5c01968f7d4f2747324eb"}, + {file = "Cython-0.29.36-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:fdf377b0f6e9325b73ad88933136023184afdc795caeeaaf3dca13494cffd15e"}, + {file = "Cython-0.29.36-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ff2cc5518558c598028ae8d9a43401e0e734b74b6e598156b005328c9da3472"}, + {file = "Cython-0.29.36-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7ca921068242cd8b52544870c807fe285c1f248b12df7b6dfae25cc9957b965e"}, + {file = "Cython-0.29.36-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6058a6d04e04d790cda530e1ff675e9352359eb4b777920df3cac2b62a9a030f"}, + {file = "Cython-0.29.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:de2045ceae1857e56a72f08e0acfa48c994277a353b7bdab1f097db9f8803f19"}, + {file = "Cython-0.29.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9f2a4b4587aaef08815410dc20653613ca04a120a2954a92c39e37c6b5fdf6be"}, + {file = "Cython-0.29.36-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2edd9f8edca69178d74cbbbc180bc3e848433c9b7dc80374a11a0bb0076c926d"}, + {file = "Cython-0.29.36-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c6c0aea8491a70f98b7496b5057c9523740e02cec21cd678eef609d2aa6c1257"}, + {file = "Cython-0.29.36-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:768f65b16d23c630d8829ce1f95520ef1531a9c0489fa872d87c8c3813f65aee"}, + {file = "Cython-0.29.36-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:568625e8274ee7288ad87b0f615ec36ab446ca9b35e77481ed010027d99c7020"}, + {file = "Cython-0.29.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bdc0a4cb99f55e6878d4b67a4bfee23823484915cb6b7e9c9dd01002dd3592ea"}, + {file = "Cython-0.29.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f0df6552be39853b10dfb5a10dbd08f5c49023d6b390d7ce92d4792a8b6e73ee"}, + {file = "Cython-0.29.36-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:8894db6f5b6479a3c164e0454e13083ebffeaa9a0822668bb2319bdf1b783df1"}, + {file = "Cython-0.29.36-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53f93a8c342e9445a8f0cb7039775294f2dbbe5241936573daeaf0afe30397e4"}, + {file = "Cython-0.29.36-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ee317f9bcab901a3db39c34ee5a27716f7132e5c0de150125342694d18b30f51"}, + {file = "Cython-0.29.36-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e4b8269e5a5d127a2191b02b9df3636c0dac73f14f1ff8a831f39cb5197c4f38"}, + {file = "Cython-0.29.36-py2.py3-none-any.whl", hash = "sha256:95bb13d8be507425d03ebe051f90d4b2a9fdccc64e4f30b35645fdb7542742eb"}, + {file = "Cython-0.29.36.tar.gz", hash = "sha256:41c0cfd2d754e383c9eeb95effc9aa4ab847d0c9747077ddd7c0dcb68c3bc01f"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "exceptiongroup" +version = "1.1.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "filetype" +version = "1.2.0" +description = "Infer file type and MIME type of any file/buffer. No external dependencies." +optional = true +python-versions = "*" +files = [ + {file = "filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25"}, + {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "greenlet" +version = "2.0.2" +description = "Lightweight in-process concurrent programming" +optional = true +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +files = [ + {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, + {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, + {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, + {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, + {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, + {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, + {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, + {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, + {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, + {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, + {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, + {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, + {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, + {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, + {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, + {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, + {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, + {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, + {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, + {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, + {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, + {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, + {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, + {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, + {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, + {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, + {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, + {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, + {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, + {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, + {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, + {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, + {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, + {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, +] + +[package.extras] +docs = ["Sphinx", "docutils (<0.18)"] +test = ["objgraph", "psutil"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, + {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, +] + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "jmespath" +version = "0.9.5" +description = "JSON Matching Expressions" +optional = false +python-versions = "*" +files = [ + {file = "jmespath-0.9.5-py2.py3-none-any.whl", hash = "sha256:695cb76fa78a10663425d5b73ddc5714eb711157e52704d69be03b1a02ba4fec"}, + {file = "jmespath-0.9.5.tar.gz", hash = "sha256:cca55c8d153173e21baa59983015ad0daf603f9cb799904ff057bfb8ff8dc2d9"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "loguru" +version = "0.4.1" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.4.1-py3-none-any.whl", hash = "sha256:074b3caa6748452c1e4f2b302093c94b65d5a4c5a4d7743636b4121e06437b0e"}, + {file = "loguru-0.4.1.tar.gz", hash = "sha256:a6101fd435ac89ba5205a105a26a6ede9e4ddbb4408a6e167852efca47806d11"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (>=2.2.1)", "black (>=19.3b0)", "codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=4.3.20)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)", "tox-travis (>=0.12)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "markupsafe" +version = "2.1.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "platformdirs" +version = "3.9.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"}, + {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.3", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "ply" +version = "3.11" +description = "Python Lex & Yacc" +optional = true +python-versions = "*" +files = [ + {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, + {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "pydantic" +version = "1.8.2" +description = "Data validation and settings management using python 3.6 type hinting" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"}, + {file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"}, + {file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"}, + {file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"}, + {file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"}, + {file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"}, + {file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"}, + {file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"}, + {file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"}, + {file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"}, + {file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"}, + {file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"}, + {file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"}, + {file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"}, +] + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "pymysql" +version = "1.1.0" +description = "Pure Python MySQL Driver" +optional = true +python-versions = ">=3.7" +files = [ + {file = "PyMySQL-1.1.0-py3-none-any.whl", hash = "sha256:8969ec6d763c856f7073c4c64662882675702efcb114b4bcbb955aea3a069fa7"}, + {file = "PyMySQL-1.1.0.tar.gz", hash = "sha256:4f13a7df8bf36a51e81dd9f3605fede45a4878fe02f9236349fd82a3f0612f96"}, +] + +[package.extras] +ed25519 = ["PyNaCl (>=1.4.0)"] +rsa = ["cryptography"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "pytest-html" +version = "3.2.0" +description = "pytest plugin for generating HTML reports" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pytest-html-3.2.0.tar.gz", hash = "sha256:c4e2f4bb0bffc437f51ad2174a8a3e71df81bbc2f6894604e604af18fbe687c3"}, + {file = "pytest_html-3.2.0-py3-none-any.whl", hash = "sha256:868c08564a68d8b2c26866f1e33178419bb35b1e127c33784a28622eb827f3f3"}, +] + +[package.dependencies] +py = ">=1.8.2" +pytest = ">=5.0,<6.0.0 || >6.0.0" +pytest-metadata = "*" + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "pytest-metadata" +version = "3.0.0" +description = "pytest plugin for test session metadata" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_metadata-3.0.0-py3-none-any.whl", hash = "sha256:a17b1e40080401dc23177599208c52228df463db191c1a573ccdffacd885e190"}, + {file = "pytest_metadata-3.0.0.tar.gz", hash = "sha256:769a9c65d2884bd583bc626b0ace77ad15dbe02dd91a9106d47fd46d9c2569ca"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "tox (>=3.24.5)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "requests-toolbelt" +version = "0.10.1" +description = "A utility belt for advanced users of python-requests" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-0.10.1.tar.gz", hash = "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d"}, + {file = "requests_toolbelt-0.10.1-py2.py3-none-any.whl", hash = "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "sentry-sdk" +version = "0.14.4" +description = "Python client for Sentry (https://getsentry.com)" +optional = false +python-versions = "*" +files = [ + {file = "sentry-sdk-0.14.4.tar.gz", hash = "sha256:0e5e947d0f7a969314aa23669a94a9712be5a688ff069ff7b9fc36c66adc160c"}, + {file = "sentry_sdk-0.14.4-py2.py3-none-any.whl", hash = "sha256:799a8bf76b012e3030a881be00e97bc0b922ce35dde699c6537122b751d80e2c"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.10.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +beam = ["beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)"] +pyspark = ["pyspark (>=2.4.4)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +tornado = ["tornado (>=5)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "sqlalchemy" +version = "1.4.49" +description = "Database Abstraction Library" +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "SQLAlchemy-1.4.49-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e126cf98b7fd38f1e33c64484406b78e937b1a280e078ef558b95bf5b6895f6"}, + {file = "SQLAlchemy-1.4.49-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:03db81b89fe7ef3857b4a00b63dedd632d6183d4ea5a31c5d8a92e000a41fc71"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:95b9df9afd680b7a3b13b38adf6e3a38995da5e162cc7524ef08e3be4e5ed3e1"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63e43bf3f668c11bb0444ce6e809c1227b8f067ca1068898f3008a273f52b09"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f835c050ebaa4e48b18403bed2c0fda986525896efd76c245bdd4db995e51a4c"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c21b172dfb22e0db303ff6419451f0cac891d2e911bb9fbf8003d717f1bcf91"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-win32.whl", hash = "sha256:5fb1ebdfc8373b5a291485757bd6431de8d7ed42c27439f543c81f6c8febd729"}, + {file = "SQLAlchemy-1.4.49-cp310-cp310-win_amd64.whl", hash = "sha256:f8a65990c9c490f4651b5c02abccc9f113a7f56fa482031ac8cb88b70bc8ccaa"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8923dfdf24d5aa8a3adb59723f54118dd4fe62cf59ed0d0d65d940579c1170a4"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9ab2c507a7a439f13ca4499db6d3f50423d1d65dc9b5ed897e70941d9e135b0"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5debe7d49b8acf1f3035317e63d9ec8d5e4d904c6e75a2a9246a119f5f2fdf3d"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-win32.whl", hash = "sha256:82b08e82da3756765c2e75f327b9bf6b0f043c9c3925fb95fb51e1567fa4ee87"}, + {file = "SQLAlchemy-1.4.49-cp311-cp311-win_amd64.whl", hash = "sha256:171e04eeb5d1c0d96a544caf982621a1711d078dbc5c96f11d6469169bd003f1"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:36e58f8c4fe43984384e3fbe6341ac99b6b4e083de2fe838f0fdb91cebe9e9cb"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b31e67ff419013f99ad6f8fc73ee19ea31585e1e9fe773744c0f3ce58c039c30"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c14b29d9e1529f99efd550cd04dbb6db6ba5d690abb96d52de2bff4ed518bc95"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40f3470e084d31247aea228aa1c39bbc0904c2b9ccbf5d3cfa2ea2dac06f26d"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-win32.whl", hash = "sha256:706bfa02157b97c136547c406f263e4c6274a7b061b3eb9742915dd774bbc264"}, + {file = "SQLAlchemy-1.4.49-cp36-cp36m-win_amd64.whl", hash = "sha256:a7f7b5c07ae5c0cfd24c2db86071fb2a3d947da7bd487e359cc91e67ac1c6d2e"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:4afbbf5ef41ac18e02c8dc1f86c04b22b7a2125f2a030e25bbb4aff31abb224b"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24e300c0c2147484a002b175f4e1361f102e82c345bf263242f0449672a4bccf"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:201de072b818f8ad55c80d18d1a788729cccf9be6d9dc3b9d8613b053cd4836d"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653ed6817c710d0c95558232aba799307d14ae084cc9b1f4c389157ec50df5c"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-win32.whl", hash = "sha256:647e0b309cb4512b1f1b78471fdaf72921b6fa6e750b9f891e09c6e2f0e5326f"}, + {file = "SQLAlchemy-1.4.49-cp37-cp37m-win_amd64.whl", hash = "sha256:ab73ed1a05ff539afc4a7f8cf371764cdf79768ecb7d2ec691e3ff89abbc541e"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:37ce517c011560d68f1ffb28af65d7e06f873f191eb3a73af5671e9c3fada08a"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1878ce508edea4a879015ab5215546c444233881301e97ca16fe251e89f1c55"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e8e608983e6f85d0852ca61f97e521b62e67969e6e640fe6c6b575d4db68557"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccf956da45290df6e809ea12c54c02ace7f8ff4d765d6d3dfb3655ee876ce58d"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-win32.whl", hash = "sha256:f167c8175ab908ce48bd6550679cc6ea20ae169379e73c7720a28f89e53aa532"}, + {file = "SQLAlchemy-1.4.49-cp38-cp38-win_amd64.whl", hash = "sha256:45806315aae81a0c202752558f0df52b42d11dd7ba0097bf71e253b4215f34f4"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b6d0c4b15d65087738a6e22e0ff461b407533ff65a73b818089efc8eb2b3e1de"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a843e34abfd4c797018fd8d00ffffa99fd5184c421f190b6ca99def4087689bd"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c890421651b45a681181301b3497e4d57c0d01dc001e10438a40e9a9c25ee77"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d26f280b8f0a8f497bc10573849ad6dc62e671d2468826e5c748d04ed9e670d5"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-win32.whl", hash = "sha256:ec2268de67f73b43320383947e74700e95c6770d0c68c4e615e9897e46296294"}, + {file = "SQLAlchemy-1.4.49-cp39-cp39-win_amd64.whl", hash = "sha256:bbdf16372859b8ed3f4d05f925a984771cd2abd18bd187042f24be4886c2a15f"}, + {file = "SQLAlchemy-1.4.49.tar.gz", hash = "sha256:06ff25cbae30c396c4b7737464f2a7fc37a67b7da409993b182b024cec80aed9"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] +mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql", "pymysql (<1)"] +sqlcipher = ["sqlcipher3-binary"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "thrift" +version = "0.16.0" +description = "Python bindings for the Apache Thrift RPC system" +optional = true +python-versions = "*" +files = [ + {file = "thrift-0.16.0.tar.gz", hash = "sha256:2b5b6488fcded21f9d312aa23c9ff6a0195d0f6ae26ddbd5ad9e3e25dfc14408"}, +] + +[package.dependencies] +six = ">=1.7.2" + +[package.extras] +all = ["tornado (>=4.0)", "twisted"] +tornado = ["tornado (>=4.0)"] +twisted = ["twisted"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "thriftpy2" +version = "0.4.16" +description = "Pure python implementation of Apache Thrift." +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "thriftpy2-0.4.16.tar.gz", hash = "sha256:2aa67ecda99a948e4146341d388260b48ee7da5dfb9a951c4151988e2ed2fb4c"}, +] + +[package.dependencies] +ply = ">=3.4,<4.0" +six = ">=1.15,<2.0" + +[package.extras] +dev = ["cython (>=0.28.4)", "flake8 (>=2.5)", "pytest (>=2.8)", "pytest (>=6.1.1)", "sphinx (>=1.3)", "sphinx-rtd-theme (>=0.1.9)", "tornado (>=4.0,<6.0)"] +tornado = ["tornado (>=4.0,<6.0)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "typed-ast" +version = "1.5.5" +description = "a fork of Python 2 and 3 ast modules with type comment support" +optional = false +python-versions = ">=3.6" +files = [ + {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, + {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, + {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, + {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, + {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, + {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, + {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, + {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, + {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, + {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, + {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, + {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, + {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, + {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, + {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "urllib3" +version = "1.26.16" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, + {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[extras] +allure = ["allure-pytest"] +sql = ["pymysql", "sqlalchemy"] +thrift = ["cython", "thrift", "thriftpy2"] +upload = ["filetype", "requests-toolbelt"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.7" +content-hash = "c7281ee1e83f6cfc1c0e341084c86be11342200c94e7fa87faaeabd0d658d41f" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..11d9ccd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,75 @@ +[tool.poetry] +name = "httprunner" +version = "v4.3.5" +description = "One-stop solution for HTTP(S) testing." +license = "Apache-2.0" +readme = "README.md" +authors = ["debugtalk "] + +homepage = "https://httprunner.com" +repository = "https://github.com/httprunner/httprunner" +documentation = "https://httprunner.com/docs" + +keywords = ["HTTP", "apitest", "perftest", "requests"] + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Libraries :: Python Modules", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10" +] + +include = ["docs/CHANGELOG.md"] + +[tool.poetry.dependencies] +python = "^3.7" +pydantic = "~1.8" # >=1.8.0 <1.9.0 +loguru = "^0.4.1" +jmespath = "^0.9.5" +black = "^22.3.0" +pytest = "^7.1.1" +pytest-html = "^3.1.1" +sentry-sdk = "^0.14.4" +allure-pytest = {version = "^2.8.16", optional = true} +requests-toolbelt = {version = "^0.10.1", optional = true} +filetype = {version = "^1.0.7", optional = true} +Brotli = "^1.0.9" +jinja2 = "^3.0.3" +toml = "^0.10.2" +sqlalchemy = {version = "^1.4.36", optional = true} +pymysql = {version = "^1.0.2",optional = true} +cython = {version = "^0.29.28", optional = true} +thriftpy2 = {version = "^0.4.14", optional = true} +thrift = {version = "^0.16.0", optional = true} +pyyaml = "^6.0.1" +requests = "^2.31.0" +urllib3 = "^1.26" + +[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 +sql = ["sqlalchemy","pymysql"] # pip install "httprunner[sql]", poetry install -E sql +thrift = ["cython","thrift","thriftpy2"] # pip install "httprunner[thrift]", poetry install -E sql + +[tool.poetry.dev-dependencies] +coverage = "^4.5.4" + +[tool.poetry.scripts] +httprunner = "httprunner.cli:main" +hrun = "httprunner.cli:main_hrun_alias" +hmake = "httprunner.cli:main_make_alias" + +[build-system] +requires = ["poetry>=1.0.0"] +build-backend = "poetry.masonry.api" + +[[tool.poetry.source]] +name = "tsinghua" +url = "https://pypi.tuna.tsinghua.edu.cn/simple/"