From e5790722f8f7e9dea19c878b0e7acdb8e7f4a776 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 29 Feb 2024 18:12:51 +0800 Subject: [PATCH 01/46] =?UTF-8?q?=E6=B7=BB=E5=8A=A0copier=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- copier.yaml | 0 pdm.lock | 164 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 copier.yaml diff --git a/copier.yaml b/copier.yaml new file mode 100644 index 0000000..e69de29 diff --git a/pdm.lock b/pdm.lock index d2fce2b..3f020a2 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "test"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:65d15407cef9b261cf054e1851e8a3a12928f070d558484d5b42829b68f7b680" +content_hash = "sha256:acf570f5fb73b46cbb3f06a0cb7dc8a7b2d4980411141120372143c1ecab1249" [[package]] name = "allure-pytest" @@ -285,6 +285,33 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "copier" +version = "9.1.1" +requires_python = ">=3.8" +summary = "A library for rendering project templates." +dependencies = [ + "colorama>=0.4.6", + "decorator>=5.1.1", + "dunamai>=1.7.0; python_version < \"4\"", + "funcy>=1.17", + "jinja2-ansible-filters>=1.3.1", + "jinja2>=3.1.3", + "packaging>=23.0", + "pathspec>=0.9.0", + "plumbum>=1.6.9", + "pydantic>=2.4.2", + "pygments>=2.7.1", + "pyyaml-include>=1.2", + "pyyaml>=5.3.1", + "questionary>=1.8.1", + "typing-extensions<5.0.0,>=3.7.4; python_version < \"3.9\"", +] +files = [ + {file = "copier-9.1.1-py3-none-any.whl", hash = "sha256:d6983127a7d1af7970bf8d5a50948ca6057a592a87ed4b39eaf6a86b8746fa2a"}, + {file = "copier-9.1.1.tar.gz", hash = "sha256:1127771e33d00453bdc716ab314b06c11bd0925815ab1e1010c03278d7934ebd"}, +] + [[package]] name = "cryptography" version = "41.0.6" @@ -328,6 +355,16 @@ files = [ {file = "DBUtils-3.0.2.tar.gz", hash = "sha256:fadeb979e1406dc123e2db9955f314e0d5360f304e0bd6cf047aaa5fc3fdf5b3"}, ] +[[package]] +name = "decorator" +version = "5.1.1" +requires_python = ">=3.5" +summary = "Decorators for Humans" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + [[package]] name = "dirty-equals" version = "0.7.1" @@ -350,6 +387,19 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] +[[package]] +name = "dunamai" +version = "1.19.2" +requires_python = ">=3.5" +summary = "Dynamic version generation" +dependencies = [ + "packaging>=20.9", +] +files = [ + {file = "dunamai-1.19.2-py3-none-any.whl", hash = "sha256:bc126b17571a44d68ed826cec596e0f61dc01edca8b21486f70014936a5d44f2"}, + {file = "dunamai-1.19.2.tar.gz", hash = "sha256:3be4049890763e19b8df1d52960dbea60b3e263eb0c96144a677ae0633734d2e"}, +] + [[package]] name = "eval-type-backport" version = "0.1.3" @@ -394,6 +444,15 @@ files = [ {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, ] +[[package]] +name = "funcy" +version = "2.0" +summary = "A fancy and practical functional tools" +files = [ + {file = "funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0"}, + {file = "funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb"}, +] + [[package]] name = "h11" version = "0.12.0" @@ -596,6 +655,19 @@ files = [ {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] +[[package]] +name = "jinja2-ansible-filters" +version = "1.3.2" +summary = "A port of Ansible's jinja2 filters without requiring ansible core." +dependencies = [ + "Jinja2", + "PyYAML", +] +files = [ + {file = "jinja2-ansible-filters-1.3.2.tar.gz", hash = "sha256:07c10cf44d7073f4f01102ca12d9a2dc31b41d47e4c61ed92ef6a6d2669b356b"}, + {file = "jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34"}, +] + [[package]] name = "jsonschema" version = "4.21.1" @@ -757,6 +829,16 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +requires_python = ">=3.8" +summary = "Utility library for gitignore style pattern matching of file paths." +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "pkgutil-resolve-name" version = "1.3.10" @@ -787,6 +869,19 @@ files = [ {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] +[[package]] +name = "plumbum" +version = "1.8.2" +requires_python = ">=3.6" +summary = "Plumbum: shell combinators library" +dependencies = [ + "pywin32; platform_system == \"Windows\" and platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "plumbum-1.8.2-py3-none-any.whl", hash = "sha256:3ad9e5f56c6ec98f6f7988f7ea8b52159662ea9e915868d369dbccbfca0e367e"}, + {file = "plumbum-1.8.2.tar.gz", hash = "sha256:9e6dc032f4af952665f32f3206567bc23b7858b1413611afe603a3f8ad9bfd75"}, +] + [[package]] name = "pre-commit" version = "3.2.2" @@ -804,6 +899,19 @@ files = [ {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"}, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +requires_python = ">=3.6.2" +summary = "Library for building powerful interactive command lines in Python" +dependencies = [ + "wcwidth", +] +files = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] + [[package]] name = "py" version = "1.11.0" @@ -1063,6 +1171,25 @@ files = [ {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] +[[package]] +name = "pywin32" +version = "306" +summary = "Python for Window Extensions" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + [[package]] name = "pyyaml" version = "6.0.1" @@ -1110,6 +1237,32 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "pyyaml-include" +version = "1.3.2" +requires_python = ">=3.7" +summary = "Extending PyYAML with a custom constructor for including YAML files within YAML files" +dependencies = [ + "PyYAML<7.0,>=6.0", +] +files = [ + {file = "pyyaml-include-1.3.2.tar.gz", hash = "sha256:a516d5172092ec110a427dafe171da9341fe488eb6d1c78fb52f1f0414dec26d"}, + {file = "pyyaml_include-1.3.2-py3-none-any.whl", hash = "sha256:cc0c0a1e4c2a91e4f0b1aea83634fe8b622fe90fc3be8e8293f1e47c5ed6001b"}, +] + +[[package]] +name = "questionary" +version = "2.0.1" +requires_python = ">=3.8" +summary = "Python library to build pretty command line user prompts ⭐️" +dependencies = [ + "prompt-toolkit<=3.0.36,>=2.0", +] +files = [ + {file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"}, + {file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"}, +] + [[package]] name = "redis" version = "5.0.1" @@ -1499,6 +1652,15 @@ files = [ {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] +[[package]] +name = "wcwidth" +version = "0.2.13" +summary = "Measures the displayed width of unicode strings in a terminal" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [[package]] name = "win32-setctime" version = "1.1.0" diff --git a/pyproject.toml b/pyproject.toml index 91e4228..9293ce3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dependencies = [ "eval-type-backport>=0.1.3", "stamina==24.1.0", "jsonschema>=4.21.1", + "copier>=9.1.1", ] requires-python = ">=3.8" readme = "README.md" From a1910c504de9e2a1371ec19b4b0a105497c6ff5d Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 1 Mar 2024 11:01:33 +0800 Subject: [PATCH 02/46] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=8E=9F=E5=A7=8BCLI?= =?UTF-8?q?=E7=94=A8=E4=BA=8E=E6=9E=84=E5=BB=BA=E5=9F=BA=E7=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- pdm.lock | 206 +++++----------------------------------- pyproject.toml | 1 - requirements.txt | 4 +- 4 files changed, 25 insertions(+), 188 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef6bf18..4a6ab74 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: check-json - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.2.0 + rev: v0.3.0 hooks: - id: ruff args: diff --git a/pdm.lock b/pdm.lock index 3f020a2..33cb74d 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "test"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:acf570f5fb73b46cbb3f06a0cb7dc8a7b2d4980411141120372143c1ecab1249" +content_hash = "sha256:65d15407cef9b261cf054e1851e8a3a12928f070d558484d5b42829b68f7b680" [[package]] name = "allure-pytest" @@ -93,7 +93,7 @@ files = [ [[package]] name = "cappa" -version = "0.16.4" +version = "0.16.5" requires_python = ">=3.8,<4" summary = "Declarative CLI argument parser." dependencies = [ @@ -103,8 +103,8 @@ dependencies = [ "typing-inspect>=0.9.0", ] files = [ - {file = "cappa-0.16.4-py3-none-any.whl", hash = "sha256:5053b5c915b063642d144f92bd4aa76f735a1ecf3e8bea4c7a616c48910b1efe"}, - {file = "cappa-0.16.4.tar.gz", hash = "sha256:97ca23e0fbbcfea7ce4aa3dc181de0b18e2005d32e01b2163ccb778ebeeb5e5e"}, + {file = "cappa-0.16.5-py3-none-any.whl", hash = "sha256:8dd0547c962aa249d271ac47cc6f0a0b1819e92d5e715a7a5ec8019ab60e39c1"}, + {file = "cappa-0.16.5.tar.gz", hash = "sha256:bfe803368812433897ccfe953db4fa11b0c800be58387e588531e9a23ff83ba4"}, ] [[package]] @@ -285,33 +285,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "copier" -version = "9.1.1" -requires_python = ">=3.8" -summary = "A library for rendering project templates." -dependencies = [ - "colorama>=0.4.6", - "decorator>=5.1.1", - "dunamai>=1.7.0; python_version < \"4\"", - "funcy>=1.17", - "jinja2-ansible-filters>=1.3.1", - "jinja2>=3.1.3", - "packaging>=23.0", - "pathspec>=0.9.0", - "plumbum>=1.6.9", - "pydantic>=2.4.2", - "pygments>=2.7.1", - "pyyaml-include>=1.2", - "pyyaml>=5.3.1", - "questionary>=1.8.1", - "typing-extensions<5.0.0,>=3.7.4; python_version < \"3.9\"", -] -files = [ - {file = "copier-9.1.1-py3-none-any.whl", hash = "sha256:d6983127a7d1af7970bf8d5a50948ca6057a592a87ed4b39eaf6a86b8746fa2a"}, - {file = "copier-9.1.1.tar.gz", hash = "sha256:1127771e33d00453bdc716ab314b06c11bd0925815ab1e1010c03278d7934ebd"}, -] - [[package]] name = "cryptography" version = "41.0.6" @@ -355,16 +328,6 @@ files = [ {file = "DBUtils-3.0.2.tar.gz", hash = "sha256:fadeb979e1406dc123e2db9955f314e0d5360f304e0bd6cf047aaa5fc3fdf5b3"}, ] -[[package]] -name = "decorator" -version = "5.1.1" -requires_python = ">=3.5" -summary = "Decorators for Humans" -files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] - [[package]] name = "dirty-equals" version = "0.7.1" @@ -387,19 +350,6 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] -[[package]] -name = "dunamai" -version = "1.19.2" -requires_python = ">=3.5" -summary = "Dynamic version generation" -dependencies = [ - "packaging>=20.9", -] -files = [ - {file = "dunamai-1.19.2-py3-none-any.whl", hash = "sha256:bc126b17571a44d68ed826cec596e0f61dc01edca8b21486f70014936a5d44f2"}, - {file = "dunamai-1.19.2.tar.gz", hash = "sha256:3be4049890763e19b8df1d52960dbea60b3e263eb0c96144a677ae0633734d2e"}, -] - [[package]] name = "eval-type-backport" version = "0.1.3" @@ -444,15 +394,6 @@ files = [ {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, ] -[[package]] -name = "funcy" -version = "2.0" -summary = "A fancy and practical functional tools" -files = [ - {file = "funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0"}, - {file = "funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb"}, -] - [[package]] name = "h11" version = "0.12.0" @@ -655,19 +596,6 @@ files = [ {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] -[[package]] -name = "jinja2-ansible-filters" -version = "1.3.2" -summary = "A port of Ansible's jinja2 filters without requiring ansible core." -dependencies = [ - "Jinja2", - "PyYAML", -] -files = [ - {file = "jinja2-ansible-filters-1.3.2.tar.gz", hash = "sha256:07c10cf44d7073f4f01102ca12d9a2dc31b41d47e4c61ed92ef6a6d2669b356b"}, - {file = "jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34"}, -] - [[package]] name = "jsonschema" version = "4.21.1" @@ -829,16 +757,6 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -requires_python = ">=3.8" -summary = "Utility library for gitignore style pattern matching of file paths." -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "pkgutil-resolve-name" version = "1.3.10" @@ -869,19 +787,6 @@ files = [ {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] -[[package]] -name = "plumbum" -version = "1.8.2" -requires_python = ">=3.6" -summary = "Plumbum: shell combinators library" -dependencies = [ - "pywin32; platform_system == \"Windows\" and platform_python_implementation != \"PyPy\"", -] -files = [ - {file = "plumbum-1.8.2-py3-none-any.whl", hash = "sha256:3ad9e5f56c6ec98f6f7988f7ea8b52159662ea9e915868d369dbccbfca0e367e"}, - {file = "plumbum-1.8.2.tar.gz", hash = "sha256:9e6dc032f4af952665f32f3206567bc23b7858b1413611afe603a3f8ad9bfd75"}, -] - [[package]] name = "pre-commit" version = "3.2.2" @@ -899,19 +804,6 @@ files = [ {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"}, ] -[[package]] -name = "prompt-toolkit" -version = "3.0.36" -requires_python = ">=3.6.2" -summary = "Library for building powerful interactive command lines in Python" -dependencies = [ - "wcwidth", -] -files = [ - {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, - {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, -] - [[package]] name = "py" version = "1.11.0" @@ -1171,25 +1063,6 @@ files = [ {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] -[[package]] -name = "pywin32" -version = "306" -summary = "Python for Window Extensions" -files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, -] - [[package]] name = "pyyaml" version = "6.0.1" @@ -1237,32 +1110,6 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] -[[package]] -name = "pyyaml-include" -version = "1.3.2" -requires_python = ">=3.7" -summary = "Extending PyYAML with a custom constructor for including YAML files within YAML files" -dependencies = [ - "PyYAML<7.0,>=6.0", -] -files = [ - {file = "pyyaml-include-1.3.2.tar.gz", hash = "sha256:a516d5172092ec110a427dafe171da9341fe488eb6d1c78fb52f1f0414dec26d"}, - {file = "pyyaml_include-1.3.2-py3-none-any.whl", hash = "sha256:cc0c0a1e4c2a91e4f0b1aea83634fe8b622fe90fc3be8e8293f1e47c5ed6001b"}, -] - -[[package]] -name = "questionary" -version = "2.0.1" -requires_python = ">=3.8" -summary = "Python library to build pretty command line user prompts ⭐️" -dependencies = [ - "prompt-toolkit<=3.0.36,>=2.0", -] -files = [ - {file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"}, - {file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"}, -] - [[package]] name = "redis" version = "5.0.1" @@ -1517,27 +1364,27 @@ files = [ [[package]] name = "ruff" -version = "0.2.2" +version = "0.3.0" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, - {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, - {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, - {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, - {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, + {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944"}, + {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f"}, + {file = "ruff-0.3.0-py3-none-win32.whl", hash = "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b"}, + {file = "ruff-0.3.0-py3-none-win_amd64.whl", hash = "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f"}, + {file = "ruff-0.3.0-py3-none-win_arm64.whl", hash = "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"}, + {file = "ruff-0.3.0.tar.gz", hash = "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a"}, ] [[package]] @@ -1652,15 +1499,6 @@ files = [ {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] -[[package]] -name = "wcwidth" -version = "0.2.13" -summary = "Measures the displayed width of unicode strings in a terminal" -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - [[package]] name = "win32-setctime" version = "1.1.0" diff --git a/pyproject.toml b/pyproject.toml index 9293ce3..91e4228 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,6 @@ dependencies = [ "eval-type-backport>=0.1.3", "stamina==24.1.0", "jsonschema>=4.21.1", - "copier>=9.1.1", ] requires-python = ">=3.8" readme = "README.md" diff --git a/requirements.txt b/requirements.txt index cb8dcc6..f181c4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ anyio==3.7.1 async-timeout==4.0.3; python_full_version <= "3.11.2" attrs==23.2.0 cache3==0.4.3 -cappa==0.16.4 +cappa==0.16.5 certifi==2023.11.17 cffi==1.16.0 cfgv==3.4.0 @@ -67,7 +67,7 @@ rfc3986==1.5.0 rich==13.6.0 rpds-py==0.18.0 rtoml==0.9.0 -ruff==0.2.2 +ruff==0.3.0 setuptools==69.0.3 six==1.16.0 sniffio==1.3.0 From 554e54db47c30db5cc66ba0d9265437c0b632f7c Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 1 Mar 2024 13:21:45 +0800 Subject: [PATCH 03/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0ruff=E8=A7=84=E5=88=99?= =?UTF-8?q?=E5=B9=B6=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ruff.toml | 39 ++++++++++++++++--- httpfpt/cli.py | 43 +++------------------ httpfpt/common/json_handler.py | 8 +--- httpfpt/common/send_request.py | 16 ++------ httpfpt/run.py | 15 ++++--- httpfpt/utils/assert_control.py | 24 +++++------- httpfpt/utils/data_manage/apifox.py | 2 +- httpfpt/utils/enum_control.py | 3 +- httpfpt/utils/relate_testcase_executor.py | 12 ++---- httpfpt/utils/request/request_data_parse.py | 18 +++------ 10 files changed, 71 insertions(+), 109 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index eaf282b..c0ec3a5 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,25 +1,50 @@ line-length = 120 cache-dir = "./httpfpt/.ruff_cache" unsafe-fixes = true +target-version = "py310" # 代码风格版本 [lint] select = [ "E", "F", "I", + "W", + "N", + "FA", "TCH", - "W505", - "PT018", - "SIM101", - "SIM114", - "PGH004", - "RUF100", + "T20", + "FURB", + # UP + "UP004", + "UP006", + "UP007", + "UP008", + "UP034", + "UP037", + "UP039", + # ANN "ANN001", "ANN201", "ANN202", "ANN204", "ANN205", "ANN206", + # PIE + "PIE810", + # PT + "PT018", + # SIM + "SIM101", + "SIM114", + # PGH + "PGH004", + # PRF + "PERF101", + "PERF102", + # RUF + "RUF100", + "RUF013", + "RUF027", ] [lint.flake8-pytest-style] @@ -46,3 +71,5 @@ mypy-init-return = true [format] quote-style = "single" +docstring-code-format = true +skip-magic-trailing-comma = true diff --git a/httpfpt/cli.py b/httpfpt/cli.py index 4b33268..428cb78 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -167,15 +167,7 @@ def cmd_run_test_parse(value: Value) -> bool | Value: @cappa.command(name='httpfpt-cli') @dataclass class HttpFptCLI: - version: Annotated[ - bool, - cappa.Arg( - short='-V', - long=True, - default=False, - help='Print version information.', - ), - ] + version: Annotated[bool, cappa.Arg(short='-V', long=True, default=False, help='Print version information.')] run_test: Annotated[ list[str] | None, cappa.Arg( @@ -216,14 +208,7 @@ class TestCaseCLI: ), ] generate: Annotated[ - bool, - cappa.Arg( - short='-gt', - long=True, - default=False, - help='自动生成测试用例.', - required=False, - ), + bool, cappa.Arg(short='-gt', long=True, default=False, help='自动生成测试用例.', required=False) ] def __call__(self) -> None: @@ -260,33 +245,15 @@ class ImportCLI: ] har: Annotated[ tuple[str, str], - cappa.Arg( - short='-ih', - long='--import-har', - default=(), - help='TODO: Not started yet.', - required=False, - ), + cappa.Arg(short='-ih', long='--import-har', default=(), help='TODO: Not started yet.', required=False), ] jmeter: Annotated[ tuple[str, str], - cappa.Arg( - short='-ij', - long='--import-jmeter', - default=(), - help='TODO: Not started yet.', - required=False, - ), + cappa.Arg(short='-ij', long='--import-jmeter', default=(), help='TODO: Not started yet.', required=False), ] postman: Annotated[ tuple[str, str], - cappa.Arg( - short='-ip', - long='--import-postman', - default=(), - help='TODO: Not started yet.', - required=False, - ), + cappa.Arg(short='-ip', long='--import-postman', default=(), help='TODO: Not started yet.', required=False), ] git: Annotated[ str, diff --git a/httpfpt/common/json_handler.py b/httpfpt/common/json_handler.py index 0199a54..e894ef5 100644 --- a/httpfpt/common/json_handler.py +++ b/httpfpt/common/json_handler.py @@ -41,13 +41,7 @@ def read_json_file(filepath: str | None = CASE_DATA_PATH, *, filename: str, **kw def write_json_file( - filepath: str | None = None, - *, - filename: str, - data: Any = None, - encoding: str = 'utf-8', - mode: str = 'a', - **kwargs, + filepath: str | None = None, *, filename: str, data: Any = None, encoding: str = 'utf-8', mode: str = 'a', **kwargs ) -> None: """ 写入 json 文件 diff --git a/httpfpt/common/send_request.py b/httpfpt/common/send_request.py index 5c24b4f..1cd387c 100644 --- a/httpfpt/common/send_request.py +++ b/httpfpt/common/send_request.py @@ -48,9 +48,7 @@ def init_response_metadata(self) -> dict: 'json': None, 'content': None, 'text': None, - 'stat': { - 'execute_time': None, - }, + 'stat': {'execute_time': None}, } return response_metadata @@ -327,7 +325,7 @@ def log_request_teardown(self, teardown: list) -> None: def log_request_down(response_data: dict) -> None: log.info(f"请求发送时间: {response_data['stat']['execute_time']}") str_status_code = str(response_data['status_code']) - if str_status_code.startswith('4') or str_status_code.startswith('5'): + if str_status_code.startswith(('4', '5')): log.error(f"响应状态码: {response_data['status_code']}") else: log.info(f"响应状态码: {response_data['status_code']}") @@ -363,13 +361,7 @@ def allure_request_teardown(teardown_log: dict) -> None: @staticmethod def allure_request_down(response_data: dict) -> None: - allure_step( - '响应数据', - { - 'status_code': response_data['status_code'], - 'elapsed': response_data['elapsed'], - }, - ) + allure_step('响应数据', {'status_code': response_data['status_code'], 'elapsed': response_data['elapsed']}) @staticmethod def allure_dynamic_data(parsed_data: dict) -> None: @@ -379,7 +371,7 @@ def allure_dynamic_data(parsed_data: dict) -> None: if parsed_data['allure_severity'] is not None: allure.dynamic.severity(parsed_data['allure_severity']) if parsed_data['files_no_parse'] is not None: - for k, v in parsed_data['files_no_parse'].items(): + for v in parsed_data['files_no_parse'].values(): if isinstance(v, list): for path in v: allure_attach_file(path) diff --git a/httpfpt/run.py b/httpfpt/run.py index 5a5e878..ec84f60 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -192,15 +192,14 @@ def run( logo = """\n /$$ /$$ /$$$$$$$$ /$$$$$$$$ /$$$$$$$ /$$$$$$$$ /$$$$$$$ /$$$$$$$$ | $$ | $$|__ $$__/|__ $$__/| $$__ $$| $$_____/| $$__ $$|__ $$__/ - | $$ | $$ | $$ | $$ | $$ | $$| $$ | $$ | $$ | $$ - | $$$$$$$$ | $$ | $$ | $$$$$$$/| $$$$$$ | $$$$$$$/ | $$ - | $$__ $$ | $$ | $$ | $$____/ | $$___/ | $$____/ | $$ - | $$ | $$ | $$ | $$ | $$ | $$ | $$ | $$ - | $$ | $$ | $$ | $$ | $$ | $$ | $$ | $$ - |__/ |__/ |__/ |__/ |__/ |__/ |__/ |__/ - + | $$ | $$ | $$ | $$ | $$ | $$| $$ | $$ | $$ | $$ + | $$$$$$$$ | $$ | $$ | $$$$$$$/| $$$$$$ | $$$$$$$/ | $$ + | $$__ $$ | $$ | $$ | $$____/ | $$___/ | $$____/ | $$ + | $$ | $$ | $$ | $$ | $$ | $$ | $$ | $$ + | $$ | $$ | $$ | $$ | $$ | $$ | $$ | $$ + |__/ |__/ |__/ |__/ |__/ |__/ |__/ |__/ + """ - print(logo) log.info(logo) redis_client.init() case_data.clean_cache_data(clean_cache) diff --git a/httpfpt/utils/assert_control.py b/httpfpt/utils/assert_control.py index c6cae8d..84bfe47 100644 --- a/httpfpt/utils/assert_control.py +++ b/httpfpt/utils/assert_control.py @@ -40,18 +40,16 @@ def _code_asserter(self, response: dict, assert_text: str) -> None: 取值范围:: { - "url": '', - "status_code": 200, - "elapsed": 0, - "headers": {}, - "cookies": {}, - "result": {}, - "content": {}, - "text": {}, - "stat": { - "execute_time": "None" - }, - "sql_data": {} + 'url': '', + 'status_code': 200, + 'elapsed': 0, + 'headers': {}, + 'cookies': {}, + 'result': {}, + 'content': {}, + 'text': {}, + 'stat': {'execute_time': 'None'}, + 'sql_data': {}, } 一些简单的例子:: @@ -210,8 +208,6 @@ def _exec_code_assert(response: dict, assert_text: str) -> None: :return: """ assert_split = assert_text.split(' ') - if assert_text.startswith("'{") or assert_text.startswith("'["): - raise AssertSyntaxError('code 断言内容语法错误, 请查看是否为 str / list 类型, 并检查断言语法是否符合规范') if not assert_text.startswith('assert '): raise AssertSyntaxError(f'code 断言取值表达式格式错误, 不符合语法规范: {assert_text}') if len(assert_split) < 4 or len(assert_split) > 6: diff --git a/httpfpt/utils/data_manage/apifox.py b/httpfpt/utils/data_manage/apifox.py index 267d6b3..b81e015 100644 --- a/httpfpt/utils/data_manage/apifox.py +++ b/httpfpt/utils/data_manage/apifox.py @@ -70,7 +70,7 @@ def import_apifox_to_yaml(self, source: str, project: str | None = None) -> None ) # 写入项目根目录 if len(root_case) > 0: - for _, v in root_case.items(): + for v in root_case.values(): case_file_data = {'config': case_config, 'test_steps': v} write_yaml( CASE_DATA_PATH, diff --git a/httpfpt/utils/enum_control.py b/httpfpt/utils/enum_control.py index 39269f1..3e7357e 100644 --- a/httpfpt/utils/enum_control.py +++ b/httpfpt/utils/enum_control.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from enum import Enum -from typing import Type -def get_enum_values(enum_class: Type[Enum]) -> list: +def get_enum_values(enum_class: type[Enum]) -> list: return list(map(lambda ec: ec.value, enum_class)) diff --git a/httpfpt/utils/relate_testcase_executor.py b/httpfpt/utils/relate_testcase_executor.py index a1347b0..d86c77f 100644 --- a/httpfpt/utils/relate_testcase_executor.py +++ b/httpfpt/utils/relate_testcase_executor.py @@ -68,9 +68,7 @@ def exec_setup_testcase(parsed_data: dict, setup_testcase: str | dict) -> dict | response = relate_testcase_exec_with_new_request_data(case_data) # 使用更新请求数据后的请求响应提取变量 if setup_testcase.get('response') is not None: - testcase_data = { - 'set_var_response': setup_testcase['response'], - } + testcase_data = {'set_var_response': setup_testcase['response']} relate_testcase_extract_with_response(testcase_data, response) else: if setup_testcase.get('response') is not None: @@ -88,15 +86,11 @@ def exec_setup_testcase(parsed_data: dict, setup_testcase: str | dict) -> dict | case_data.update(testcase_data) response = relate_testcase_exec_with_new_request_data(case_data) if setup_testcase.get('response') is not None: - testcase_data = { - 'set_var_response': setup_testcase['response'], - } + testcase_data = {'set_var_response': setup_testcase['response']} relate_testcase_extract_with_response(testcase_data, response) else: if setup_testcase.get('response') is not None: - testcase_data = { - 'set_var_response': setup_testcase['response'], - } + testcase_data = {'set_var_response': setup_testcase['response']} case_data.update(testcase_data) relate_testcase_extract(case_data) diff --git a/httpfpt/utils/request/request_data_parse.py b/httpfpt/utils/request/request_data_parse.py index ac2725e..09d4eeb 100644 --- a/httpfpt/utils/request/request_data_parse.py +++ b/httpfpt/utils/request/request_data_parse.py @@ -167,7 +167,7 @@ def proxies(self) -> dict | None: raise RequestDataParseError( _error_msg('测试用例数据解析失败,参数 config:request:proxies 不符合规范') ) - for k, v in proxies.items(): + for v in proxies.values(): if v is not None: if not isinstance(v, str): raise RequestDataParseError( @@ -555,9 +555,7 @@ def _setup_sql(index: int, sql: str | dict | None) -> str | dict | None: mysql_client.sql_verify(v) else: if not isinstance(sql, str): - raise RequestDataParseError( - _error_msg(f'参数 test_steps:setup:sql[{index}] 不是有效的 str 类型'), - ) + raise RequestDataParseError(_error_msg(f'参数 test_steps:setup:sql[{index}] 不是有效的 str 类型')) else: mysql_client.sql_verify(sql) return sql @@ -566,9 +564,7 @@ def _setup_sql(index: int, sql: str | dict | None) -> str | dict | None: def _setup_hook(index: int, hook: str | None) -> str | None: if hook is not None: if not isinstance(hook, str): - raise RequestDataParseError( - _error_msg(f'参数 test_steps:setup:hook[{index}] 不是有效的 str 类型'), - ) + raise RequestDataParseError(_error_msg(f'参数 test_steps:setup:hook[{index}] 不是有效的 str 类型')) return hook @staticmethod @@ -621,14 +617,14 @@ def _teardown_sql(index: int, sql: str | dict | None) -> str | dict | None: for k, v in sql.items(): if not isinstance(v, str): raise RequestDataParseError( - _error_msg(f'参数 test_steps:teardown:sql[{index}]:{k} 不是有效的 str 类型'), + _error_msg(f'参数 test_steps:teardown:sql[{index}]:{k} 不是有效的 str 类型') ) if k == 'sql': mysql_client.sql_verify(v) else: if not isinstance(sql, str): raise RequestDataParseError( - _error_msg(f'参数 test_steps:teardown:sql[{index}] 不是有效的 str 类型'), + _error_msg(f'参数 test_steps:teardown:sql[{index}] 不是有效的 str 类型') ) else: mysql_client.sql_verify(sql) @@ -638,9 +634,7 @@ def _teardown_sql(index: int, sql: str | dict | None) -> str | dict | None: def _teardown_hook(index: int, hook: str | None) -> str | None: if hook is not None: if not isinstance(hook, str): - raise RequestDataParseError( - _error_msg(f'参数 test_steps:teardown:hook[{index}] 不是有效的 str 类型'), - ) + raise RequestDataParseError(_error_msg(f'参数 test_steps:teardown:hook[{index}] 不是有效的 str 类型')) return hook @staticmethod From 22c6ff1f707a73676b9050f7841ffd3ed85f930c Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 1 Mar 2024 16:39:03 +0800 Subject: [PATCH 04/46] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=AF=BB=E5=8F=96=E4=B8=BA=E5=8A=A8=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/common/errors.py | 7 ++ httpfpt/common/excel_handler.py | 2 +- httpfpt/common/log.py | 2 +- httpfpt/common/toml_handler.py | 7 +- httpfpt/core/get_conf.py | 126 +++++++++++++++++--------------- httpfpt/run.py | 10 ++- 6 files changed, 90 insertions(+), 64 deletions(-) diff --git a/httpfpt/common/errors.py b/httpfpt/common/errors.py index 4e0c02d..ecfee6c 100644 --- a/httpfpt/common/errors.py +++ b/httpfpt/common/errors.py @@ -12,6 +12,13 @@ def __str__(self) -> str: return self.msg +class ConfigInitError(HttpFptErrorMixin, FileNotFoundError): + """配置初始化错误""" + + def __init__(self, msg: str) -> None: + super().__init__(msg) + + class AuthError(HttpFptErrorMixin, ValueError): """认证错误""" diff --git a/httpfpt/common/excel_handler.py b/httpfpt/common/excel_handler.py index ac4adb7..b1db5b8 100644 --- a/httpfpt/common/excel_handler.py +++ b/httpfpt/common/excel_handler.py @@ -11,7 +11,7 @@ from httpfpt.common.log import log -def read_excel(filepath: str, *, filename: str, sheet: str = 'Sheet1') -> list[dict[str, Any | None]]: +def read_excel(filepath: str, *, filename: str, sheet: str) -> list[dict[str, Any | None] | None]: """ 读取 xlsx 文件 diff --git a/httpfpt/common/log.py b/httpfpt/common/log.py index 01507df..826e8c1 100644 --- a/httpfpt/common/log.py +++ b/httpfpt/common/log.py @@ -31,7 +31,7 @@ def log() -> loguru.Logger: if not os.path.join(LOG_PATH): os.makedirs(LOG_PATH) - log_file = os.path.join(LOG_PATH, 'api_test.log') + log_file = os.path.join(LOG_PATH, 'httpfpt.log') # 清除 logger 配置 logger.remove() diff --git a/httpfpt/common/toml_handler.py b/httpfpt/common/toml_handler.py index 9b7f5f6..a2aeeb0 100644 --- a/httpfpt/common/toml_handler.py +++ b/httpfpt/common/toml_handler.py @@ -7,7 +7,7 @@ from httpfpt.common.log import log -def read_toml(filepath: str, filename: str, encoding: str = 'utf-8') -> dict: +def read_toml(filepath: str, filename: str | None, encoding: str = 'utf-8') -> dict: """ 读取 toml 文件 @@ -16,9 +16,10 @@ def read_toml(filepath: str, filename: str, encoding: str = 'utf-8') -> dict: :param encoding: 文件内容编码格式 :return: """ - _filename = os.path.join(filepath, filename) + if filename: + filepath = os.path.join(filepath, filename) try: - with open(_filename, encoding=encoding) as f: + with open(filepath, encoding=encoding) as f: data = rtoml.load(f) except ValueError as e: log.error(f'文件 {filename} 内容在格式错误 \n {e}') diff --git a/httpfpt/core/get_conf.py b/httpfpt/core/get_conf.py index d4872dd..f2c0d00 100644 --- a/httpfpt/core/get_conf.py +++ b/httpfpt/core/get_conf.py @@ -1,96 +1,108 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from functools import lru_cache -from pathlib import Path +from __future__ import annotations from httpfpt.common.toml_handler import read_toml +__all__ = ['config', 'init_config'] -class Config: - def __init__(self) -> None: - self.__config = read_toml(str(Path(__file__).resolve().parent), 'conf.toml') +# global config +config = None + + +class HttpFptConfig: + def __init__(self, settings: dict) -> None: + """ + 初始化配置 + + :param settings: + """ # 项目目录名 - self.PROJECT_NAME = self.__config['project']['project'] + self.PROJECT_NAME = settings['project']['project'] # 测试报告 - self.TEST_REPORT_TITLE = self.__config['report']['title'] - self.TESTER_NAME = self.__config['report']['tester_name'] + self.TEST_REPORT_TITLE = settings['report']['title'] + self.TESTER_NAME = settings['report']['tester_name'] # mysql 数据库 - self.MYSQL_HOST = self.__config['mysql']['host'] - self.MYSQL_PORT = self.__config['mysql']['port'] - self.MYSQL_USER = self.__config['mysql']['user'] - self.MYSQL_PASSWORD = self.__config['mysql']['password'] - self.MYSQL_DATABASE = self.__config['mysql']['database'] - self.MYSQL_CHARSET = self.__config['mysql']['charset'] + self.MYSQL_HOST = settings['mysql']['host'] + self.MYSQL_PORT = settings['mysql']['port'] + self.MYSQL_USER = settings['mysql']['user'] + self.MYSQL_PASSWORD = settings['mysql']['password'] + self.MYSQL_DATABASE = settings['mysql']['database'] + self.MYSQL_CHARSET = settings['mysql']['charset'] # redis 数据库 - self.REDIS_HOST = self.__config['redis']['host'] - self.REDIS_PORT = self.__config['redis']['port'] - self.REDIS_PASSWORD = self.__config['redis']['password'] - self.REDIS_DATABASE = self.__config['redis']['database'] - self.REDIS_TIMEOUT = self.__config['redis']['timeout'] + self.REDIS_HOST = settings['redis']['host'] + self.REDIS_PORT = settings['redis']['port'] + self.REDIS_PASSWORD = settings['redis']['password'] + self.REDIS_DATABASE = settings['redis']['database'] + self.REDIS_TIMEOUT = settings['redis']['timeout'] # 邮件 - self.EMAIL_SERVER = self.__config['email']['host_server'] - self.EMAIL_PORT = self.__config['email']['port'] - self.EMAIL_USER = self.__config['email']['user'] - self.EMAIL_PASSWORD = self.__config['email']['password'] - self.EMAIL_SEND_TO = self.__config['email']['send_to'] - self.EMAIL_SSL = self.__config['email']['ssl'] - self.EMAIL_REPORT_SEND = self.__config['email']['send_report'] + self.EMAIL_SERVER = settings['email']['host_server'] + self.EMAIL_PORT = settings['email']['port'] + self.EMAIL_USER = settings['email']['user'] + self.EMAIL_PASSWORD = settings['email']['password'] + self.EMAIL_SEND_TO = settings['email']['send_to'] + self.EMAIL_SSL = settings['email']['ssl'] + self.EMAIL_REPORT_SEND = settings['email']['send_report'] # 钉钉 - self.DING_TALK_WEBHOOK = self.__config['ding_talk']['webhook'] + self.DING_TALK_WEBHOOK = settings['ding_talk']['webhook'] self.DING_TALK_PROXY = { - 'http': self.__config['ding_talk']['proxies']['http'] - if self.__config['ding_talk']['proxies']['http'] != '' + 'http': settings['ding_talk']['proxies']['http'] + if settings['ding_talk']['proxies']['http'] != '' else None, - 'https': self.__config['ding_talk']['proxies']['https'] - if self.__config['ding_talk']['proxies']['https'] != '' + 'https': settings['ding_talk']['proxies']['https'] + if settings['ding_talk']['proxies']['https'] != '' else None, } - self.DING_TALK_REPORT_SEND = self.__config['ding_talk']['send_report'] + self.DING_TALK_REPORT_SEND = settings['ding_talk']['send_report'] # 飞书 - self.LARK_TALK_WEBHOOK = self.__config['lark_talk']['webhook'] + self.LARK_TALK_WEBHOOK = settings['lark_talk']['webhook'] self.LARK_TALK_PROXY = { - 'http': self.__config['lark_talk']['proxies']['http'] - if self.__config['lark_talk']['proxies']['http'] != '' + 'http': settings['lark_talk']['proxies']['http'] + if settings['lark_talk']['proxies']['http'] != '' else None, - 'https': self.__config['lark_talk']['proxies']['https'] - if self.__config['lark_talk']['proxies']['https'] != '' + 'https': settings['lark_talk']['proxies']['https'] + if settings['lark_talk']['proxies']['https'] != '' else None, } - self.LARK_TALK_REPORT_SEND = self.__config['lark_talk']['send_report'] + self.LARK_TALK_REPORT_SEND = settings['lark_talk']['send_report'] # 请求发送 - self.REQUEST_TIMEOUT = self.__config['request']['timeout'] - self.REQUEST_VERIFY = self.__config['request']['verify'] - self.REQUEST_REDIRECTS = self.__config['request']['redirects'] + self.REQUEST_TIMEOUT = settings['request']['timeout'] + self.REQUEST_VERIFY = settings['request']['verify'] + self.REQUEST_REDIRECTS = settings['request']['redirects'] self.REQUEST_PROXIES_REQUESTS = { - 'http': self.__config['request']['proxies']['http'] - if self.__config['request']['proxies']['http'] != '' - else None, - 'https': self.__config['request']['proxies']['https'] - if self.__config['request']['proxies']['https'] != '' - else None, + 'http': settings['request']['proxies']['http'] if settings['request']['proxies']['http'] != '' else None, + 'https': settings['request']['proxies']['https'] if settings['request']['proxies']['https'] != '' else None, } self.REQUEST_PROXIES_HTTPX = { - 'http://': self.__config['request']['proxies']['http'] - if self.__config['request']['proxies']['http'] != '' - else None, - 'https://': self.__config['request']['proxies']['https'] - if self.__config['request']['proxies']['https'] != '' + 'http://': settings['request']['proxies']['http'] if settings['request']['proxies']['http'] != '' else None, + 'https://': settings['request']['proxies']['https'] + if settings['request']['proxies']['https'] != '' else None, } - self.REQUEST_RETRY = self.__config['request']['retry'] + self.REQUEST_RETRY = settings['request']['retry'] + +def init_config(settings: str | dict, config_filename: str | None = None) -> None: + """ + 初始化配置 -@lru_cache(maxsize=None) -def cache_config() -> Config: - return Config() + :param settings: 项目配置,字典或指定 toml 配置路径文件 + :param config_filename: + :return: + """ + global config + if isinstance(settings, str): + conf = read_toml(settings, config_filename) + else: + conf = settings -config = cache_config() + config = HttpFptConfig(conf) diff --git a/httpfpt/run.py b/httpfpt/run.py index ec84f60..0b25c55 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -14,7 +14,7 @@ from httpfpt.common.log import log from httpfpt.common.yaml_handler import read_yaml -from httpfpt.core.get_conf import config +from httpfpt.core.get_conf import config, init_config from httpfpt.core.path_conf import ( ALLURE_ENV_FILE, ALLURE_REPORT_ENV_FILE, @@ -148,6 +148,8 @@ def startup( def run( *args, # init + settings: str | dict, + config_filename: str | None = None, clean_cache: bool = False, pydantic_verify: bool = True, # log level @@ -171,6 +173,9 @@ def run( """ 运行入口 + :param args: pytest 运行参数 + :param settings: 项目核心配置,字典或指定配置路径文件 + :param config_filename: 当 settings 指定配置路径时,可单独指定配置文件名 :param clean_cache: 清理 redis 缓存数据,对于脏数据,这很有用,默认关闭 :param pydantic_verify: 用例数据完整架构 pydantic 快速检测, 默认开启 :param args: pytest 运行参数 @@ -185,7 +190,7 @@ def run( :param strict_markers: markers 严格模式, 对于设置 marker 装饰器的用例, 如果 marker 未在 pytest.ini 注册, 用例将报错 :param capture: 避免在使用输出模式为"v"和"s"时,html报告中的表格日志为空的情况, 默认开启 :param disable_warnings: 关闭控制台警告信息, 默认开启 - :param kwargs: pytest 运行参数 + :param kwargs: pytest 运行关键字参数 :return: """ try: @@ -201,6 +206,7 @@ def run( """ log.info(logo) + init_config(settings, config_filename) redis_client.init() case_data.clean_cache_data(clean_cache) case_data.case_data_init(pydantic_verify) From 9f8d81b4f31b6239b48eac0940cbbc0e42d10a3d Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 1 Mar 2024 19:05:37 +0800 Subject: [PATCH 05/46] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=BA=E5=8A=A8=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/common/json_handler.py | 4 +- httpfpt/common/log.py | 10 ++- httpfpt/common/yaml_handler.py | 17 ++-- httpfpt/core/get_conf.py | 10 +-- httpfpt/core/path_conf.py | 95 +++++++++++---------- httpfpt/db/mysql_db.py | 4 +- httpfpt/run.py | 32 +++---- httpfpt/utils/auth_plugins.py | 4 +- httpfpt/utils/case_auto_generator.py | 8 +- httpfpt/utils/data_manage/apifox.py | 6 +- httpfpt/utils/data_manage/openapi.py | 10 +-- httpfpt/utils/file_control.py | 10 ++- httpfpt/utils/request/request_data_parse.py | 4 +- httpfpt/utils/request/vars_extractor.py | 8 +- httpfpt/utils/send_report/send_email.py | 6 +- 15 files changed, 115 insertions(+), 113 deletions(-) diff --git a/httpfpt/common/json_handler.py b/httpfpt/common/json_handler.py index e894ef5..62cdbe0 100644 --- a/httpfpt/common/json_handler.py +++ b/httpfpt/common/json_handler.py @@ -8,10 +8,10 @@ from typing import Any from httpfpt.common.log import log -from httpfpt.core.path_conf import CASE_DATA_PATH +from httpfpt.core.path_conf import path_config -def read_json_file(filepath: str | None = CASE_DATA_PATH, *, filename: str, **kwargs) -> dict: +def read_json_file(filepath: str | None = path_config.CASE_DATA_PATH, *, filename: str, **kwargs) -> dict: """ 读取 json 文件 diff --git a/httpfpt/common/log.py b/httpfpt/common/log.py index 826e8c1..2d2010f 100644 --- a/httpfpt/common/log.py +++ b/httpfpt/common/log.py @@ -9,7 +9,7 @@ from loguru import logger -from httpfpt.core.path_conf import LOG_PATH +from httpfpt.core.path_conf import path_config if TYPE_CHECKING: import loguru @@ -28,10 +28,12 @@ def log() -> loguru.Logger: :return: """ - if not os.path.join(LOG_PATH): - os.makedirs(LOG_PATH) + log_path = path_config.LOG_PATH - log_file = os.path.join(LOG_PATH, 'httpfpt.log') + if not os.path.join(log_path): + os.makedirs(log_path) + + log_file = os.path.join(log_path, 'httpfpt.log') # 清除 logger 配置 logger.remove() diff --git a/httpfpt/common/yaml_handler.py b/httpfpt/common/yaml_handler.py index 390c1b1..7b33da6 100644 --- a/httpfpt/common/yaml_handler.py +++ b/httpfpt/common/yaml_handler.py @@ -10,11 +10,11 @@ import yaml from httpfpt.common.log import log -from httpfpt.core.path_conf import CASE_DATA_PATH, TEST_DATA_PATH, YAML_REPORT_PATH +from httpfpt.core.path_conf import path_config from httpfpt.utils.time_control import get_current_time -def read_yaml(filepath: str | None = CASE_DATA_PATH, *, filename: str) -> dict[str, Any]: +def read_yaml(filepath: str | None = path_config.CASE_DATA_PATH, *, filename: str) -> dict[str, Any]: """ 读取 yaml 文件 @@ -83,9 +83,10 @@ def write_yaml_report( :param mode: 文件写入模式 :return """ - if not os.path.exists(YAML_REPORT_PATH): - os.makedirs(YAML_REPORT_PATH) - _file = os.path.join(YAML_REPORT_PATH, filename) + yaml_report_path = path_config.YAML_REPORT_PATH + if not os.path.exists(yaml_report_path): + os.makedirs(yaml_report_path) + _file = os.path.join(yaml_report_path, filename) try: with open(_file, encoding=encoding, mode=mode) as f: yaml.dump(data, f, allow_unicode=True) @@ -103,9 +104,9 @@ def write_yaml_vars(data: dict) -> None: :param data: :return: """ - _file = os.path.join(TEST_DATA_PATH, 'global_vars.yaml') + _file = os.path.join(path_config.TEST_DATA_PATH, 'global_vars.yaml') try: - _vars = read_yaml(TEST_DATA_PATH, filename='global_vars.yaml') + _vars = read_yaml(path_config.TEST_DATA_PATH, filename='global_vars.yaml') _vars.update(data) with open(_file, encoding='utf-8', mode='w') as f: yaml.dump(_vars, f, allow_unicode=True) @@ -115,7 +116,7 @@ def write_yaml_vars(data: dict) -> None: log.info(f'写入全局变量成功: global_vars.yaml -> {data}') -def get_yaml_file(filepath: str = CASE_DATA_PATH, *, filename: str) -> str: +def get_yaml_file(filepath: str = path_config.CASE_DATA_PATH, *, filename: str) -> str: """ 获取 yaml 测试数据文件 diff --git a/httpfpt/core/get_conf.py b/httpfpt/core/get_conf.py index f2c0d00..c33cf53 100644 --- a/httpfpt/core/get_conf.py +++ b/httpfpt/core/get_conf.py @@ -13,18 +13,15 @@ class HttpFptConfig: def __init__(self, settings: dict) -> None: """ - 初始化配置 + 项目配置初始化 :param settings: """ - # 项目目录名 self.PROJECT_NAME = settings['project']['project'] - # 测试报告 self.TEST_REPORT_TITLE = settings['report']['title'] self.TESTER_NAME = settings['report']['tester_name'] - # mysql 数据库 self.MYSQL_HOST = settings['mysql']['host'] self.MYSQL_PORT = settings['mysql']['port'] @@ -32,14 +29,12 @@ def __init__(self, settings: dict) -> None: self.MYSQL_PASSWORD = settings['mysql']['password'] self.MYSQL_DATABASE = settings['mysql']['database'] self.MYSQL_CHARSET = settings['mysql']['charset'] - # redis 数据库 self.REDIS_HOST = settings['redis']['host'] self.REDIS_PORT = settings['redis']['port'] self.REDIS_PASSWORD = settings['redis']['password'] self.REDIS_DATABASE = settings['redis']['database'] self.REDIS_TIMEOUT = settings['redis']['timeout'] - # 邮件 self.EMAIL_SERVER = settings['email']['host_server'] self.EMAIL_PORT = settings['email']['port'] @@ -48,7 +43,6 @@ def __init__(self, settings: dict) -> None: self.EMAIL_SEND_TO = settings['email']['send_to'] self.EMAIL_SSL = settings['email']['ssl'] self.EMAIL_REPORT_SEND = settings['email']['send_report'] - # 钉钉 self.DING_TALK_WEBHOOK = settings['ding_talk']['webhook'] self.DING_TALK_PROXY = { @@ -60,7 +54,6 @@ def __init__(self, settings: dict) -> None: else None, } self.DING_TALK_REPORT_SEND = settings['ding_talk']['send_report'] - # 飞书 self.LARK_TALK_WEBHOOK = settings['lark_talk']['webhook'] self.LARK_TALK_PROXY = { @@ -72,7 +65,6 @@ def __init__(self, settings: dict) -> None: else None, } self.LARK_TALK_REPORT_SEND = settings['lark_talk']['send_report'] - # 请求发送 self.REQUEST_TIMEOUT = settings['request']['timeout'] self.REQUEST_VERIFY = settings['request']['verify'] diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index 01a2321..771402d 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -2,46 +2,55 @@ # -*- coding: utf-8 -*- import os -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) - -# 获取日志路径 -LOG_PATH = os.path.join(BASE_DIR, 'log') - -# 测试数据路径 -TEST_DATA_PATH = os.path.join(BASE_DIR, 'data') - -# Yaml测试数据路径 -CASE_DATA_PATH = os.path.join(TEST_DATA_PATH, 'test_data') - -# 测试报告路径 -TEST_REPORT_PATH = os.path.join(BASE_DIR, 'report') - -# allure测试报告路径 -ALLURE_REPORT_PATH = os.path.join(TEST_REPORT_PATH, 'allure_report') - -# allure html测试报告路径 -ALLURE_REPORT_HTML_PATH = os.path.join(ALLURE_REPORT_PATH, 'html') - -# HTML测试报告路径 -HTML_REPORT_PATH = os.path.join(TEST_REPORT_PATH, 'html_report') - -# HTML测试报告路径 -HTML_EMAIL_REPORT_PATH = os.path.join(BASE_DIR, 'templates') - -# YAML测试报告路径 -YAML_REPORT_PATH = os.path.join(TEST_REPORT_PATH, 'yaml_report') - -# allure环境文件 -ALLURE_ENV_FILE = os.path.join(BASE_DIR, 'core', 'allure_env', 'environment.properties') - -# allure报告环境文件,用作copy,避免allure开启清理缓存导致环境文件丢失 -ALLURE_REPORT_ENV_FILE = os.path.join(ALLURE_REPORT_PATH, 'environment.properties') - -# 运行环境文件路径 -RUN_ENV_PATH = os.path.join(BASE_DIR, 'core', 'run_env') - -# 测试用例路径 -TEST_CASE_PATH = os.path.join(BASE_DIR, 'testcases') - -# AUTH配置文件路径 -AUTH_CONF_PATH = os.path.join(BASE_DIR, 'core') +__all__ = ['path_config', 'init_path_config'] + +# global path_config +path_config = None + + +class HttpFptPathConfig: + def __init__(self, base_dir: str) -> None: + """ + 路径配置初始化 + + :param base_dir: + """ + self.BASE_DIR = base_dir + # 获取日志路径 + self.LOG_PATH = os.path.join(self.BASE_DIR, 'log') + # 测试数据路径 + self.TEST_DATA_PATH = os.path.join(self.BASE_DIR, 'data') + # Yaml测试数据路径 + self.CASE_DATA_PATH = os.path.join(self.TEST_DATA_PATH, 'test_data') + # 测试报告路径 + self.TEST_REPORT_PATH = os.path.join(self.BASE_DIR, 'report') + # allure测试报告路径 + self.ALLURE_REPORT_PATH = os.path.join(self.TEST_REPORT_PATH, 'allure_report') + # allure html测试报告路径 + self.ALLURE_REPORT_HTML_PATH = os.path.join(self.ALLURE_REPORT_PATH, 'html') + # HTML测试报告路径 + self.HTML_REPORT_PATH = os.path.join(self.TEST_REPORT_PATH, 'html_report') + # YAML测试报告路径 + self.YAML_REPORT_PATH = os.path.join(self.TEST_REPORT_PATH, 'yaml_report') + # allure环境文件 + self.ALLURE_ENV_FILE = os.path.join(self.BASE_DIR, 'core', 'allure_env', 'environment.properties') + # allure报告环境文件,用作copy,避免allure开启清理缓存导致环境文件丢失 + self.ALLURE_REPORT_ENV_FILE = os.path.join(self.ALLURE_REPORT_PATH, 'environment.properties') + # 运行环境文件路径 + self.RUN_ENV_PATH = os.path.join(self.BASE_DIR, 'core', 'run_env') + # 测试用例路径 + self.TEST_CASE_PATH = os.path.join(self.BASE_DIR, 'testcases') + # AUTH配置文件路径 + self.AUTH_CONF_PATH = os.path.join(self.BASE_DIR, 'core') + + +def init_path_config(base_dir: str) -> None: + """ + 初始化配置 + + :param base_dir: 项目根目录 + :return: + """ + global path_config + + path_config = HttpFptPathConfig(base_dir) diff --git a/httpfpt/db/mysql_db.py b/httpfpt/db/mysql_db.py index a65e5d7..8f9cb23 100644 --- a/httpfpt/db/mysql_db.py +++ b/httpfpt/db/mysql_db.py @@ -18,7 +18,7 @@ from httpfpt.common.variable_cache import variable_cache from httpfpt.common.yaml_handler import write_yaml_vars from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import RUN_ENV_PATH +from httpfpt.core.path_conf import path_config from httpfpt.enums.query_fetch_type import QueryFetchType from httpfpt.enums.sql_type import SqlType from httpfpt.enums.var_type import VarType @@ -171,7 +171,7 @@ def exec_case_sql(self, sql: str, env_filename: str | None = None) -> dict | lis if set_type == VarType.CACHE: variable_cache.set(key, value_str) elif set_type == VarType.ENV: - write_env_vars(RUN_ENV_PATH, env_filename, key, value_str) # type: ignore + write_env_vars(path_config.RUN_ENV_PATH, env_filename, key, value_str) # type: ignore elif set_type == VarType.GLOBAL: write_yaml_vars({key: value_str}) else: diff --git a/httpfpt/run.py b/httpfpt/run.py index 0b25c55..2e44573 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -15,14 +15,7 @@ from httpfpt.common.log import log from httpfpt.common.yaml_handler import read_yaml from httpfpt.core.get_conf import config, init_config -from httpfpt.core.path_conf import ( - ALLURE_ENV_FILE, - ALLURE_REPORT_ENV_FILE, - ALLURE_REPORT_HTML_PATH, - ALLURE_REPORT_PATH, - HTML_REPORT_PATH, - YAML_REPORT_PATH, -) +from httpfpt.core.path_conf import init_path_config, path_config from httpfpt.db.redis_db import redis_client from httpfpt.utils.request import case_data_parse as case_data from httpfpt.utils.send_report.ding_talk import DingTalk @@ -70,13 +63,13 @@ def startup( html_report_filename = f'{config.PROJECT_NAME}_{get_current_time("%Y-%m-%d %H_%M_%S")}.html' if html_report: - if not os.path.exists(HTML_REPORT_PATH): - os.makedirs(HTML_REPORT_PATH) - run_args.append(f'--html={os.path.join(HTML_REPORT_PATH, html_report_filename)}') + if not os.path.exists(path_config.HTML_REPORT_PATH): + os.makedirs(path_config.HTML_REPORT_PATH) + run_args.append(f'--html={os.path.join(path_config.HTML_REPORT_PATH, html_report_filename)}') run_args.append('--self-contained-html') if allure: - run_args.append(f'--alluredir={ALLURE_REPORT_PATH}') + run_args.append(f'--alluredir={path_config.ALLURE_REPORT_PATH}') if allure_clear: run_args.append('--clean-alluredir') @@ -122,9 +115,9 @@ def startup( pytest.main(run_args) log.info('🏁 FINISH') - yaml_report_files = os.listdir(YAML_REPORT_PATH) + yaml_report_files = os.listdir(path_config.YAML_REPORT_PATH) yaml_report_files.sort() - test_result = read_yaml(YAML_REPORT_PATH, filename=yaml_report_files[-1]) + test_result = read_yaml(path_config.YAML_REPORT_PATH, filename=yaml_report_files[-1]) if html_report and config.EMAIL_REPORT_SEND: SendMail(test_result, html_report_filename).send_report() @@ -136,13 +129,13 @@ def startup( LarkTalk(test_result).send() if allure: - if not os.path.exists(ALLURE_REPORT_ENV_FILE): - shutil.copyfile(ALLURE_ENV_FILE, ALLURE_REPORT_ENV_FILE) + if not os.path.exists(path_config.ALLURE_REPORT_ENV_FILE): + shutil.copyfile(path_config.ALLURE_ENV_FILE, path_config.ALLURE_REPORT_ENV_FILE) if allure and allure_serve: - os.popen(f'allure generate {ALLURE_REPORT_PATH} -o {ALLURE_REPORT_HTML_PATH} --clean') and os.popen( - f'allure serve {ALLURE_REPORT_PATH}' - ) # type: ignore + os.popen( + f'allure generate {path_config.ALLURE_REPORT_PATH} -o {path_config.ALLURE_REPORT_HTML_PATH} --clean' + ) and os.popen(f'allure serve {path_config.ALLURE_REPORT_PATH}') # type: ignore def run( @@ -207,6 +200,7 @@ def run( """ log.info(logo) init_config(settings, config_filename) + init_path_config('TODO: httpfpt startproject xxx; xxx is base_dir') redis_client.init() case_data.clean_cache_data(clean_cache) case_data.case_data_init(pydantic_verify) diff --git a/httpfpt/utils/auth_plugins.py b/httpfpt/utils/auth_plugins.py index ff75eff..b8116f0 100644 --- a/httpfpt/utils/auth_plugins.py +++ b/httpfpt/utils/auth_plugins.py @@ -8,7 +8,7 @@ from httpfpt.common.errors import AuthError from httpfpt.common.yaml_handler import read_yaml -from httpfpt.core.path_conf import AUTH_CONF_PATH +from httpfpt.core.path_conf import path_config from httpfpt.db.redis_db import redis_client from httpfpt.enums.request.auth import AuthType from httpfpt.utils.enum_control import get_enum_values @@ -24,7 +24,7 @@ def __init__(self) -> None: @lru_cache def get_auth_data(self) -> dict: - auth_data = read_yaml(AUTH_CONF_PATH, filename='auth.yaml') + auth_data = read_yaml(path_config.AUTH_CONF_PATH, filename='auth.yaml') return auth_data @property diff --git a/httpfpt/utils/case_auto_generator.py b/httpfpt/utils/case_auto_generator.py index c3ad60c..23eb71b 100644 --- a/httpfpt/utils/case_auto_generator.py +++ b/httpfpt/utils/case_auto_generator.py @@ -5,7 +5,7 @@ from pathlib import Path from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import TEST_CASE_PATH +from httpfpt.core.path_conf import path_config from httpfpt.utils.file_control import get_file_property, search_all_case_data_files, search_all_testcase_files from httpfpt.utils.rich_console import console @@ -91,9 +91,11 @@ def {testcase_func_name}(self, data): tag = case_filename.split(config.PROJECT_NAME)[1].split(os.path.sep)[1:-1] new_testcase_filename = testcase_func_name + '.py' if tag: - case_path = os.path.join(TEST_CASE_PATH, config.PROJECT_NAME, *tag, new_testcase_filename) + case_path = os.path.join( + path_config.TEST_CASE_PATH, config.PROJECT_NAME, *tag, new_testcase_filename + ) else: - case_path = os.path.join(TEST_CASE_PATH, config.PROJECT_NAME, new_testcase_filename) + case_path = os.path.join(path_config.TEST_CASE_PATH, config.PROJECT_NAME, new_testcase_filename) new_testcase_dir = Path(case_path).parent if not new_testcase_dir.exists(): new_testcase_dir.mkdir(parents=True, exist_ok=True) diff --git a/httpfpt/utils/data_manage/apifox.py b/httpfpt/utils/data_manage/apifox.py index b81e015..0a8262c 100644 --- a/httpfpt/utils/data_manage/apifox.py +++ b/httpfpt/utils/data_manage/apifox.py @@ -8,7 +8,7 @@ from httpfpt.common.json_handler import read_json_file from httpfpt.common.yaml_handler import write_yaml from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import CASE_DATA_PATH +from httpfpt.core.path_conf import path_config from httpfpt.utils.data_manage.base_format import format_value from httpfpt.utils.file_control import get_file_property from httpfpt.utils.rich_console import console @@ -63,7 +63,7 @@ def import_apifox_to_yaml(self, source: str, project: str | None = None) -> None case_config['allure']['feature'] = case_config['module'] = k case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - CASE_DATA_PATH, + path_config.CASE_DATA_PATH, os.sep.join([project or config.PROJECT_NAME, k, get_file_property(source)[1] + '.yaml']), case_file_data, mode='w', @@ -73,7 +73,7 @@ def import_apifox_to_yaml(self, source: str, project: str | None = None) -> None for v in root_case.values(): case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - CASE_DATA_PATH, + path_config.CASE_DATA_PATH, os.sep.join([project or config.PROJECT_NAME, get_file_property(source)[1] + '.yaml']), case_file_data, mode='w', diff --git a/httpfpt/utils/data_manage/openapi.py b/httpfpt/utils/data_manage/openapi.py index 429de3e..2f00f12 100644 --- a/httpfpt/utils/data_manage/openapi.py +++ b/httpfpt/utils/data_manage/openapi.py @@ -13,7 +13,7 @@ from httpfpt.common.json_handler import read_json_file from httpfpt.common.yaml_handler import write_yaml from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import CASE_DATA_PATH +from httpfpt.core.path_conf import path_config from httpfpt.utils.data_manage.base_format import format_value from httpfpt.utils.file_control import get_file_property from httpfpt.utils.rich_console import console @@ -183,7 +183,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None case_config['allure']['feature'] = case_config['module'] = k case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - CASE_DATA_PATH, + path_config.CASE_DATA_PATH, os.sep.join( [ project or config.PROJECT_NAME, @@ -213,7 +213,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None ) case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - CASE_DATA_PATH, + path_config.CASE_DATA_PATH, os.sep.join([project or config.PROJECT_NAME, filename]), case_file_data, mode='w', @@ -239,7 +239,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None ) is_write = Confirm.ask(f'❓ 是否需要创建 {tag_filename} 数据文件?', default=True) if is_write: - write_yaml(CASE_DATA_PATH, tag_filename, case_file_data, mode='w') + write_yaml(path_config.CASE_DATA_PATH, tag_filename, case_file_data, mode='w') # 写入项目根目录 if len(root_case) > 0: for k, v in root_case.items(): @@ -259,7 +259,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None if is_write: case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - CASE_DATA_PATH, + path_config.CASE_DATA_PATH, os.sep.join([project or config.PROJECT_NAME, root_filename]), case_file_data, mode='w', diff --git a/httpfpt/utils/file_control.py b/httpfpt/utils/file_control.py index f540d60..22ff5e7 100644 --- a/httpfpt/utils/file_control.py +++ b/httpfpt/utils/file_control.py @@ -8,7 +8,7 @@ from pathlib import Path from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import CASE_DATA_PATH, TEST_CASE_PATH +from httpfpt.core.path_conf import path_config def get_file_property(filepath: str) -> tuple[str, str, str]: @@ -31,7 +31,9 @@ def search_all_case_data_files(filepath: str | None = None) -> list: :return: """ - case_data_filepath = os.path.join(CASE_DATA_PATH, f'{config.PROJECT_NAME}') if filepath is None else filepath + case_data_filepath = ( + os.path.join(path_config.CASE_DATA_PATH, f'{config.PROJECT_NAME}') if filepath is None else filepath + ) files = ( glob.glob(os.path.join(case_data_filepath, '**', '*.yaml'), recursive=True) + glob.glob(os.path.join(case_data_filepath, '**', '*.yml'), recursive=True) @@ -46,7 +48,9 @@ def search_all_testcase_files() -> list: :return: """ - files = glob.glob(os.path.join(TEST_CASE_PATH, f'{config.PROJECT_NAME}', '**', 'test_*.py'), recursive=True) + files = glob.glob( + os.path.join(path_config.TEST_CASE_PATH, f'{config.PROJECT_NAME}', '**', 'test_*.py'), recursive=True + ) return files diff --git a/httpfpt/utils/request/request_data_parse.py b/httpfpt/utils/request/request_data_parse.py index 09d4eeb..803bf4c 100644 --- a/httpfpt/utils/request/request_data_parse.py +++ b/httpfpt/utils/request/request_data_parse.py @@ -15,7 +15,7 @@ from httpfpt.common.env_handler import get_env_dict from httpfpt.common.errors import RequestDataParseError from httpfpt.common.log import log -from httpfpt.core.path_conf import RUN_ENV_PATH +from httpfpt.core.path_conf import path_config from httpfpt.db.mysql_db import mysql_client from httpfpt.enums.allure_severity_type import SeverityType from httpfpt.enums.request.auth import AuthType @@ -331,7 +331,7 @@ def url(self) -> str: if not url.startswith('http'): _env = self.env try: - env_file = os.path.join(RUN_ENV_PATH, _env) + env_file = os.path.join(path_config.RUN_ENV_PATH, _env) env_dict = get_env_dict(env_file) except Exception as e: raise RequestDataParseError(f'环境变量 {_env} 读取失败: {e}') diff --git a/httpfpt/utils/request/vars_extractor.py b/httpfpt/utils/request/vars_extractor.py index 2908e90..0a09f7a 100644 --- a/httpfpt/utils/request/vars_extractor.py +++ b/httpfpt/utils/request/vars_extractor.py @@ -13,7 +13,7 @@ from httpfpt.common.log import log from httpfpt.common.variable_cache import variable_cache from httpfpt.common.yaml_handler import read_yaml, write_yaml_vars -from httpfpt.core.path_conf import RUN_ENV_PATH, TEST_DATA_PATH +from httpfpt.core.path_conf import path_config from httpfpt.enums.var_type import VarType @@ -48,13 +48,13 @@ def vars_replace(self, target: dict, env_filename: str | None = None) -> dict: if not env or not isinstance(env, str): raise RequestDataParseError('运行环境获取失败, 测试用例数据缺少 config:request:env 参数') try: - env_file = os.path.join(RUN_ENV_PATH, env) + env_file = os.path.join(path_config.RUN_ENV_PATH, env) env_vars = get_env_dict(env_file) except OSError: raise RequestDataParseError('运行环境获取失败, 请检查测试用例环境配置') # 获取全局变量 - global_vars = read_yaml(TEST_DATA_PATH, filename='global_vars.yaml') + global_vars = read_yaml(path_config.TEST_DATA_PATH, filename='global_vars.yaml') # 获取 re 规则字符串 var_re = self.sql_vars_re if env_filename else self.vars_re @@ -149,7 +149,7 @@ def teardown_var_extract(response: dict, extract: dict, env: str) -> None: if set_type == VarType.CACHE: variable_cache.set(key, value_str) elif set_type == VarType.ENV: - write_env_vars(RUN_ENV_PATH, env, key, value_str) + write_env_vars(path_config.RUN_ENV_PATH, env, key, value_str) elif set_type == VarType.GLOBAL: write_yaml_vars({key: value_str}) else: diff --git a/httpfpt/utils/send_report/send_email.py b/httpfpt/utils/send_report/send_email.py index 152165c..55628e3 100644 --- a/httpfpt/utils/send_report/send_email.py +++ b/httpfpt/utils/send_report/send_email.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- from __future__ import annotations -import os import smtplib import time @@ -14,7 +13,6 @@ from httpfpt.common.log import log from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import HTML_EMAIL_REPORT_PATH from httpfpt.enums.email_type import EmailType from httpfpt.utils.file_control import get_file_property @@ -36,7 +34,7 @@ def take_report(self) -> MIMEMultipart: self.content.update({'tester_name': config.TESTER_NAME}) # 邮件正文 - with open(os.path.join(HTML_EMAIL_REPORT_PATH, 'email_report.html'), 'r', encoding='utf-8') as f: + with open('../../templates/email_notification.html', 'r', encoding='utf-8') as f: html = Template(f.read()) mail_body = MIMEText(html.render(**self.content), _subtype='html', _charset='utf-8') @@ -64,7 +62,7 @@ def take_error(self) -> MIMEMultipart: msg['date'] = time.strftime('%a, %d %b %Y %H:%M:%S %z') # 邮件正文 - with open(os.path.join(HTML_EMAIL_REPORT_PATH, 'email_notification.html'), 'r', encoding='utf-8') as f: + with open('../../templates/email_notification.html', 'r', encoding='utf-8') as f: html = Template(f.read()) mail_body = MIMEText(html.render(**self.content), _subtype='html', _charset='utf-8') From d5b01e50b0010a2ea4920070ef95f9ad035c6bc3 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 1 Mar 2024 21:25:27 +0800 Subject: [PATCH 06/46] =?UTF-8?q?=E5=90=AF=E7=94=A8ruff=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ruff.toml | 4 +++- httpfpt/cli.py | 45 ++++++++++++++++++++++++++++++----- httpfpt/data/hooks.py | 3 ++- httpfpt/run.py | 5 ++-- httpfpt/utils/file_control.py | 3 ++- 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index c0ec3a5..4f3e4ea 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -46,6 +46,9 @@ select = [ "RUF013", "RUF027", ] +preview = true +ignore-init-module-imports = true +ignore = ["FURB101"] [lint.flake8-pytest-style] mark-parentheses = false @@ -72,4 +75,3 @@ mypy-init-return = true [format] quote-style = "single" docstring-code-format = true -skip-magic-trailing-comma = true diff --git a/httpfpt/cli.py b/httpfpt/cli.py index 428cb78..234ffaa 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -37,7 +37,7 @@ def get_version() -> None: """获取版本号""" ver = open('./__init__.py', 'rt').read() - mob = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", ver, re.M) + mob = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", ver, re.MULTILINE) if mob: console.print('\n🔥 HttpFpt', mob.group(1)) else: @@ -167,7 +167,15 @@ def cmd_run_test_parse(value: Value) -> bool | Value: @cappa.command(name='httpfpt-cli') @dataclass class HttpFptCLI: - version: Annotated[bool, cappa.Arg(short='-V', long=True, default=False, help='Print version information.')] + version: Annotated[ + bool, + cappa.Arg( + short='-V', + long=True, + default=False, + help='Print version information.', + ), + ] run_test: Annotated[ list[str] | None, cappa.Arg( @@ -208,7 +216,14 @@ class TestCaseCLI: ), ] generate: Annotated[ - bool, cappa.Arg(short='-gt', long=True, default=False, help='自动生成测试用例.', required=False) + bool, + cappa.Arg( + short='-gt', + long=True, + default=False, + help='自动生成测试用例.', + required=False, + ), ] def __call__(self) -> None: @@ -245,15 +260,33 @@ class ImportCLI: ] har: Annotated[ tuple[str, str], - cappa.Arg(short='-ih', long='--import-har', default=(), help='TODO: Not started yet.', required=False), + cappa.Arg( + short='-ih', + long='--import-har', + default=(), + help='TODO: Not started yet.', + required=False, + ), ] jmeter: Annotated[ tuple[str, str], - cappa.Arg(short='-ij', long='--import-jmeter', default=(), help='TODO: Not started yet.', required=False), + cappa.Arg( + short='-ij', + long='--import-jmeter', + default=(), + help='TODO: Not started yet.', + required=False, + ), ] postman: Annotated[ tuple[str, str], - cappa.Arg(short='-ip', long='--import-postman', default=(), help='TODO: Not started yet.', required=False), + cappa.Arg( + short='-ip', + long='--import-postman', + default=(), + help='TODO: Not started yet.', + required=False, + ), ] git: Annotated[ str, diff --git a/httpfpt/data/hooks.py b/httpfpt/data/hooks.py index e493b25..85ea09c 100644 --- a/httpfpt/data/hooks.py +++ b/httpfpt/data/hooks.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import datetime +import operator from faker import Faker @@ -22,4 +23,4 @@ def random_phone() -> str: def sum_a_b(a: int, b: int) -> int: - return a + b + return operator.add(a, b) diff --git a/httpfpt/run.py b/httpfpt/run.py index 2e44573..f191b2c 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -65,8 +65,9 @@ def startup( if html_report: if not os.path.exists(path_config.HTML_REPORT_PATH): os.makedirs(path_config.HTML_REPORT_PATH) - run_args.append(f'--html={os.path.join(path_config.HTML_REPORT_PATH, html_report_filename)}') - run_args.append('--self-contained-html') + run_args.extend( + (f'--html={os.path.join(path_config.HTML_REPORT_PATH, html_report_filename)}', '--self-contained-html') + ) if allure: run_args.append(f'--alluredir={path_config.ALLURE_REPORT_PATH}') diff --git a/httpfpt/utils/file_control.py b/httpfpt/utils/file_control.py index 22ff5e7..312d4b8 100644 --- a/httpfpt/utils/file_control.py +++ b/httpfpt/utils/file_control.py @@ -64,5 +64,6 @@ def get_file_hash(filepath: str) -> str: import hashlib with open(filepath, 'rb') as f: - file_hash = hashlib.sha256(f.read()).hexdigest() + contents = f.read() + file_hash = hashlib.sha256(contents).hexdigest() return file_hash From 3d966dbd013cd5a409b1f4cb1e5f633e4b681195 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 3 Mar 2024 20:40:52 +0800 Subject: [PATCH 07/46] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=BA=E5=8A=A8=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 77 +++++++++++++++++++++++++++------- httpfpt/common/toml_handler.py | 2 + httpfpt/run.py | 14 +++---- pdm.lock | 6 +-- requirements.txt | 2 +- 5 files changed, 72 insertions(+), 29 deletions(-) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index 234ffaa..bf87b2f 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -4,6 +4,7 @@ import os import re +import shutil import sys from dataclasses import dataclass @@ -21,7 +22,6 @@ from httpfpt.common.json_handler import read_json_file from httpfpt.common.yaml_handler import read_yaml from httpfpt.enums.case_data_type import CaseDataType -from httpfpt.run import run from httpfpt.schemas.case_data import CaseData from httpfpt.utils.case_auto_generator import auto_generate_testcases from httpfpt.utils.data_manage.apifox import ApiFoxParser @@ -164,6 +164,36 @@ def cmd_run_test_parse(value: Value) -> bool | Value: return value +def create_new_project(start_project: tuple[str | None, str | None]) -> None: + name = start_project[0] + path = start_project[1] + if path != '.': + if not os.path.isdir(path): + raise cappa.Exit(f'"{path}" is not a directory', code=1) + project_path = os.path.join(path, name) + if os.path.exists(project_path): + raise cappa.Exit('The project directory is not empty', code=1) + os.makedirs(project_path) + shutil.copytree('./core', project_path, ignore=shutil.ignore_patterns('get_conf.py', 'path_conf.py')) + shutil.copytree('./data', project_path) + shutil.copytree('./testcases', project_path) + shutil.copyfile('conftest.py', project_path) + shutil.copyfile('pytest.ini', project_path) + os.makedirs(os.path.join(project_path, '__init__.py')) + run_settings_path = os.path.join(project_path, 'core', 'conf.toml') + run_tpl = f"""#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from httpfpt import run + +BASE_DIR = Path(__file__).resolve().parent + + +run(settings={run_settings_path}, path_dir=str(BASE_DIR)) +""" + with open(os.path.join(project_path, 'run.py'), 'w', encoding='utf-8') as f: + f.write(run_tpl) + + @cappa.command(name='httpfpt-cli') @dataclass class HttpFptCLI: @@ -176,29 +206,44 @@ class HttpFptCLI: help='Print version information.', ), ] - run_test: Annotated[ - list[str] | None, + start_project: Annotated[ + tuple[str | None, str | None], cappa.Arg( - value_name='', - short='-r', - long='--run', - default=None, - help='Run test cases, do not support use with other commands, but support custom pytest running parameters,' - ' default parameters see `httpfpt/run.py`.', - parse=cmd_run_test_parse, - num_args=-1, + value_name='', + short=False, + long='--startproject', + default=('httpfpt_project', '.'), + help='Create a new project.', + required=False, ), ] + # run_test: Annotated[ + # list[str] | None, + # cappa.Arg( + # value_name='', + # short='-r', + # long='--run', + # default=None, + # help='Run test cases, do not support use with other commands, + # but support custom pytest running parameters,' + # ' default parameters see `httpfpt/run.py`.', + # parse=cmd_run_test_parse, + # num_args=-1, + # ), + # ] subcmd: Subcommands[TestCaseCLI | ImportCLI | None] = None def __call__(self) -> None: if self.version: get_version() - if self.run_test: - if self.version or self.subcmd: - console.print('\n❌ 暂不支持 -r/--run 命令与其他 CLI 命令同时使用') - raise cappa.Exit(code=1) - run(*self.run_test) if isinstance(self.run_test, list) else run() + if self.start_project: + pass + # create_new_project(self.start_project) + # if self.run_test: + # if self.version or self.subcmd: + # console.print('\n❌ 暂不支持 -r/--run 命令与其他 CLI 命令同时使用') + # raise cappa.Exit(code=1) + # run(*self.run_test) if isinstance(self.run_test, list) else run() @cappa.command(name='testcase', help='Test case tools.') diff --git a/httpfpt/common/toml_handler.py b/httpfpt/common/toml_handler.py index a2aeeb0..7f12cce 100644 --- a/httpfpt/common/toml_handler.py +++ b/httpfpt/common/toml_handler.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from __future__ import annotations + import os import rtoml diff --git a/httpfpt/run.py b/httpfpt/run.py index f191b2c..16109a7 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -142,8 +142,8 @@ def startup( def run( *args, # init + project_dir: str, settings: str | dict, - config_filename: str | None = None, clean_cache: bool = False, pydantic_verify: bool = True, # log level @@ -168,8 +168,8 @@ def run( 运行入口 :param args: pytest 运行参数 - :param settings: 项目核心配置,字典或指定配置路径文件 - :param config_filename: 当 settings 指定配置路径时,可单独指定配置文件名 + :param project_dir: 项目根目录 + :param settings: 项目核心配置,字典或指定配置文件 :param clean_cache: 清理 redis 缓存数据,对于脏数据,这很有用,默认关闭 :param pydantic_verify: 用例数据完整架构 pydantic 快速检测, 默认开启 :param args: pytest 运行参数 @@ -200,8 +200,8 @@ def run( """ log.info(logo) - init_config(settings, config_filename) - init_path_config('TODO: httpfpt startproject xxx; xxx is base_dir') + init_path_config(project_dir) + init_config(settings) redis_client.init() case_data.clean_cache_data(clean_cache) case_data.case_data_init(pydantic_verify) @@ -226,7 +226,3 @@ def run( import traceback SendMail({'error': traceback.format_exc()}).send_error() - - -if __name__ == '__main__': - run() diff --git a/pdm.lock b/pdm.lock index 33cb74d..a72d50c 100644 --- a/pdm.lock +++ b/pdm.lock @@ -93,7 +93,7 @@ files = [ [[package]] name = "cappa" -version = "0.16.5" +version = "0.16.6" requires_python = ">=3.8,<4" summary = "Declarative CLI argument parser." dependencies = [ @@ -103,8 +103,8 @@ dependencies = [ "typing-inspect>=0.9.0", ] files = [ - {file = "cappa-0.16.5-py3-none-any.whl", hash = "sha256:8dd0547c962aa249d271ac47cc6f0a0b1819e92d5e715a7a5ec8019ab60e39c1"}, - {file = "cappa-0.16.5.tar.gz", hash = "sha256:bfe803368812433897ccfe953db4fa11b0c800be58387e588531e9a23ff83ba4"}, + {file = "cappa-0.16.6-py3-none-any.whl", hash = "sha256:d3195edba6109e49597a46fc632ffca036f0ffcc8ed9c157e983fd0581c73df1"}, + {file = "cappa-0.16.6.tar.gz", hash = "sha256:14a534d58b909624623ae72547e0c09a1ef4e76bbc14592c3bb4ecc6ef394e65"}, ] [[package]] diff --git a/requirements.txt b/requirements.txt index f181c4e..7a7e899 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ anyio==3.7.1 async-timeout==4.0.3; python_full_version <= "3.11.2" attrs==23.2.0 cache3==0.4.3 -cappa==0.16.5 +cappa==0.16.6 certifi==2023.11.17 cffi==1.16.0 cfgv==3.4.0 From 5325d9d556192470621fbf50d5dc290cfd8f20c9 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 4 Mar 2024 15:45:45 +0800 Subject: [PATCH 08/46] =?UTF-8?q?=E9=87=8D=E6=9E=84=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 21 +++- httpfpt/common/errors.py | 2 +- httpfpt/common/json_handler.py | 4 +- httpfpt/common/log.py | 4 +- httpfpt/common/yaml_handler.py | 18 +-- httpfpt/core/path_conf.py | 131 ++++++++++++++------ httpfpt/db/mysql_db.py | 4 +- httpfpt/run.py | 29 ++--- httpfpt/utils/auth_plugins.py | 4 +- httpfpt/utils/case_auto_generator.py | 8 +- httpfpt/utils/data_manage/apifox.py | 6 +- httpfpt/utils/data_manage/openapi.py | 12 +- httpfpt/utils/file_control.py | 6 +- httpfpt/utils/request/request_data_parse.py | 4 +- httpfpt/utils/request/vars_extractor.py | 8 +- 15 files changed, 166 insertions(+), 95 deletions(-) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index bf87b2f..e4fc60c 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -164,7 +164,7 @@ def cmd_run_test_parse(value: Value) -> bool | Value: return value -def create_new_project(start_project: tuple[str | None, str | None]) -> None: +def create_new_project(start_project: tuple[str, str]) -> None: name = start_project[0] path = start_project[1] if path != '.': @@ -179,17 +179,27 @@ def create_new_project(start_project: tuple[str | None, str | None]) -> None: shutil.copytree('./testcases', project_path) shutil.copyfile('conftest.py', project_path) shutil.copyfile('pytest.ini', project_path) - os.makedirs(os.path.join(project_path, '__init__.py')) run_settings_path = os.path.join(project_path, 'core', 'conf.toml') + init_tpl = """#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from pathlib import Path + +from httpfpt import set_project_dir + + +BASE_DIR = Path(__file__).resolve().parent +set_project_dir(BASE_DIR) +""" run_tpl = f"""#!/usr/bin/env python3 # -*- coding: utf-8 -*- from httpfpt import run -BASE_DIR = Path(__file__).resolve().parent run(settings={run_settings_path}, path_dir=str(BASE_DIR)) """ + with open(os.path.join(project_path, '__init__.py'), 'w', encoding='utf-8') as f: + f.write(init_tpl) with open(os.path.join(project_path, 'run.py'), 'w', encoding='utf-8') as f: f.write(run_tpl) @@ -207,7 +217,7 @@ class HttpFptCLI: ), ] start_project: Annotated[ - tuple[str | None, str | None], + tuple[str, str], cappa.Arg( value_name='', short=False, @@ -237,8 +247,7 @@ def __call__(self) -> None: if self.version: get_version() if self.start_project: - pass - # create_new_project(self.start_project) + create_new_project(self.start_project) # if self.run_test: # if self.version or self.subcmd: # console.print('\n❌ 暂不支持 -r/--run 命令与其他 CLI 命令同时使用') diff --git a/httpfpt/common/errors.py b/httpfpt/common/errors.py index ecfee6c..84a8f65 100644 --- a/httpfpt/common/errors.py +++ b/httpfpt/common/errors.py @@ -12,7 +12,7 @@ def __str__(self) -> str: return self.msg -class ConfigInitError(HttpFptErrorMixin, FileNotFoundError): +class ConfigInitError(HttpFptErrorMixin, RuntimeError): """配置初始化错误""" def __init__(self, msg: str) -> None: diff --git a/httpfpt/common/json_handler.py b/httpfpt/common/json_handler.py index 62cdbe0..5f6da19 100644 --- a/httpfpt/common/json_handler.py +++ b/httpfpt/common/json_handler.py @@ -8,10 +8,10 @@ from typing import Any from httpfpt.common.log import log -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config -def read_json_file(filepath: str | None = path_config.CASE_DATA_PATH, *, filename: str, **kwargs) -> dict: +def read_json_file(filepath: str | None = httpfpt_path_config.case_data_dir, *, filename: str, **kwargs) -> dict: """ 读取 json 文件 diff --git a/httpfpt/common/log.py b/httpfpt/common/log.py index 2d2010f..6449e89 100644 --- a/httpfpt/common/log.py +++ b/httpfpt/common/log.py @@ -9,7 +9,7 @@ from loguru import logger -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config if TYPE_CHECKING: import loguru @@ -28,7 +28,7 @@ def log() -> loguru.Logger: :return: """ - log_path = path_config.LOG_PATH + log_path = httpfpt_path_config.log_dir if not os.path.join(log_path): os.makedirs(log_path) diff --git a/httpfpt/common/yaml_handler.py b/httpfpt/common/yaml_handler.py index 7b33da6..8e2e6d6 100644 --- a/httpfpt/common/yaml_handler.py +++ b/httpfpt/common/yaml_handler.py @@ -10,11 +10,11 @@ import yaml from httpfpt.common.log import log -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config from httpfpt.utils.time_control import get_current_time -def read_yaml(filepath: str | None = path_config.CASE_DATA_PATH, *, filename: str) -> dict[str, Any]: +def read_yaml(filepath: str | None = httpfpt_path_config.case_data_dir, *, filename: str) -> dict[str, Any]: """ 读取 yaml 文件 @@ -83,10 +83,10 @@ def write_yaml_report( :param mode: 文件写入模式 :return """ - yaml_report_path = path_config.YAML_REPORT_PATH - if not os.path.exists(yaml_report_path): - os.makedirs(yaml_report_path) - _file = os.path.join(yaml_report_path, filename) + _yaml_report_path = httpfpt_path_config.yaml_report_dir + if not os.path.exists(_yaml_report_path): + os.makedirs(_yaml_report_path) + _file = os.path.join(_yaml_report_path, filename) try: with open(_file, encoding=encoding, mode=mode) as f: yaml.dump(data, f, allow_unicode=True) @@ -104,9 +104,9 @@ def write_yaml_vars(data: dict) -> None: :param data: :return: """ - _file = os.path.join(path_config.TEST_DATA_PATH, 'global_vars.yaml') + _file = os.path.join(httpfpt_path_config.data_path, 'global_vars.yaml') try: - _vars = read_yaml(path_config.TEST_DATA_PATH, filename='global_vars.yaml') + _vars = read_yaml(httpfpt_path_config.data_path, filename='global_vars.yaml') _vars.update(data) with open(_file, encoding='utf-8', mode='w') as f: yaml.dump(_vars, f, allow_unicode=True) @@ -116,7 +116,7 @@ def write_yaml_vars(data: dict) -> None: log.info(f'写入全局变量成功: global_vars.yaml -> {data}') -def get_yaml_file(filepath: str = path_config.CASE_DATA_PATH, *, filename: str) -> str: +def get_yaml_file(filepath: str = httpfpt_path_config.case_data_dir, *, filename: str) -> str: """ 获取 yaml 测试数据文件 diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index 771402d..384cbd3 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -1,11 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from __future__ import annotations + import os -__all__ = ['path_config', 'init_path_config'] +from httpfpt.common.errors import ConfigInitError -# global path_config -path_config = None +__all__ = [ + 'httpfpt_path_config', + 'set_project_dir', +] class HttpFptPathConfig: @@ -15,42 +19,95 @@ def __init__(self, base_dir: str) -> None: :param base_dir: """ - self.BASE_DIR = base_dir - # 获取日志路径 - self.LOG_PATH = os.path.join(self.BASE_DIR, 'log') - # 测试数据路径 - self.TEST_DATA_PATH = os.path.join(self.BASE_DIR, 'data') - # Yaml测试数据路径 - self.CASE_DATA_PATH = os.path.join(self.TEST_DATA_PATH, 'test_data') - # 测试报告路径 - self.TEST_REPORT_PATH = os.path.join(self.BASE_DIR, 'report') - # allure测试报告路径 - self.ALLURE_REPORT_PATH = os.path.join(self.TEST_REPORT_PATH, 'allure_report') - # allure html测试报告路径 - self.ALLURE_REPORT_HTML_PATH = os.path.join(self.ALLURE_REPORT_PATH, 'html') - # HTML测试报告路径 - self.HTML_REPORT_PATH = os.path.join(self.TEST_REPORT_PATH, 'html_report') - # YAML测试报告路径 - self.YAML_REPORT_PATH = os.path.join(self.TEST_REPORT_PATH, 'yaml_report') - # allure环境文件 - self.ALLURE_ENV_FILE = os.path.join(self.BASE_DIR, 'core', 'allure_env', 'environment.properties') - # allure报告环境文件,用作copy,避免allure开启清理缓存导致环境文件丢失 - self.ALLURE_REPORT_ENV_FILE = os.path.join(self.ALLURE_REPORT_PATH, 'environment.properties') - # 运行环境文件路径 - self.RUN_ENV_PATH = os.path.join(self.BASE_DIR, 'core', 'run_env') - # 测试用例路径 - self.TEST_CASE_PATH = os.path.join(self.BASE_DIR, 'testcases') - # AUTH配置文件路径 - self.AUTH_CONF_PATH = os.path.join(self.BASE_DIR, 'core') - - -def init_path_config(base_dir: str) -> None: + self.base_dir = base_dir + + @property + def project_dir(self) -> str: + """项目根路径""" + if self.base_dir is None or not os.path.exists(self.base_dir): + self.base_dir = os.getenv('HTTPFPT_PROJECT_PATH') + if self.base_dir is None: + raise ConfigInitError( + '运行失败:在访问 httpfpt API 前,请先通过 set_project_dir() 方法设置项目根路径,' + '或配置 HTTPFPT_PROJECT_PATH 环境变量' + ) + return self.base_dir + + @property + def log_dir(self) -> str: + """日志路径""" + return os.path.join(self.project_dir, 'log') + + @property + def data_path(self) -> str: + """数据路径""" + return os.path.join(self.project_dir, 'data') + + @property + def case_data_dir(self) -> str: + """用例数据路径""" + return os.path.join(self.data_path, 'test_data') + + @property + def report_dir(self) -> str: + """测试报告路径""" + return os.path.join(self.project_dir, 'report') + + @property + def allure_report_dir(self) -> str: + """allure测试报告路径""" + return os.path.join(self.report_dir, 'allure_report') + + @property + def allure_html_report_dir(self) -> str: + """allure html测试报告路径""" + return os.path.join(self.allure_report_dir, 'html') + + @property + def html_report_dir(self) -> str: + """HTML测试报告路径""" + return os.path.join(self.report_dir, 'html_report') + + @property + def yaml_report_dir(self) -> str: + """YAML测试报告路径""" + return os.path.join(self.report_dir, 'yaml_report') + + @property + def allure_env_file(self) -> str: + """allure环境文件""" + return os.path.join(self.project_dir, 'core', 'allure_env', 'environment.properties') + + @property + def allure_report_env_file(self) -> str: + """allure报告环境文件,用作copy,避免allure开启清理缓存导致环境文件丢失""" + return os.path.join(self.allure_report_dir, 'environment.properties') + + @property + def run_env_dir(self) -> str: + """运行环境文件路径""" + return os.path.join(self.project_dir, 'core', 'run_env') + + @property + def testcase_dir(self) -> str: + """测试用例路径""" + return os.path.join(self.project_dir, 'testcases') + + @property + def auth_conf_dir(self) -> str: + """AUTH配置文件路径""" + return os.path.join(self.project_dir, 'core') + + +def set_project_dir(base_dir: str) -> HttpFptPathConfig: """ - 初始化配置 + 设置项目目录 - :param base_dir: 项目根目录 + :param base_dir: 项目根路径 :return: """ - global path_config + return HttpFptPathConfig(base_dir) - path_config = HttpFptPathConfig(base_dir) + +# global path_config +httpfpt_path_config: HttpFptPathConfig = set_project_dir('') diff --git a/httpfpt/db/mysql_db.py b/httpfpt/db/mysql_db.py index 8f9cb23..8a506ae 100644 --- a/httpfpt/db/mysql_db.py +++ b/httpfpt/db/mysql_db.py @@ -18,7 +18,7 @@ from httpfpt.common.variable_cache import variable_cache from httpfpt.common.yaml_handler import write_yaml_vars from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config from httpfpt.enums.query_fetch_type import QueryFetchType from httpfpt.enums.sql_type import SqlType from httpfpt.enums.var_type import VarType @@ -171,7 +171,7 @@ def exec_case_sql(self, sql: str, env_filename: str | None = None) -> dict | lis if set_type == VarType.CACHE: variable_cache.set(key, value_str) elif set_type == VarType.ENV: - write_env_vars(path_config.RUN_ENV_PATH, env_filename, key, value_str) # type: ignore + write_env_vars(httpfpt_path_config.run_env_dir, env_filename, key, value_str) # type: ignore elif set_type == VarType.GLOBAL: write_yaml_vars({key: value_str}) else: diff --git a/httpfpt/run.py b/httpfpt/run.py index 16109a7..5a55546 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -15,7 +15,7 @@ from httpfpt.common.log import log from httpfpt.common.yaml_handler import read_yaml from httpfpt.core.get_conf import config, init_config -from httpfpt.core.path_conf import init_path_config, path_config +from httpfpt.core.path_conf import httpfpt_path_config from httpfpt.db.redis_db import redis_client from httpfpt.utils.request import case_data_parse as case_data from httpfpt.utils.send_report.ding_talk import DingTalk @@ -63,14 +63,17 @@ def startup( html_report_filename = f'{config.PROJECT_NAME}_{get_current_time("%Y-%m-%d %H_%M_%S")}.html' if html_report: - if not os.path.exists(path_config.HTML_REPORT_PATH): - os.makedirs(path_config.HTML_REPORT_PATH) + if not os.path.exists(httpfpt_path_config.html_report_dir): + os.makedirs(httpfpt_path_config.html_report_dir) run_args.extend( - (f'--html={os.path.join(path_config.HTML_REPORT_PATH, html_report_filename)}', '--self-contained-html') + ( + f'--html={os.path.join(httpfpt_path_config.html_report_dir, html_report_filename)}', + '--self-contained-html', + ) ) if allure: - run_args.append(f'--alluredir={path_config.ALLURE_REPORT_PATH}') + run_args.append(f'--alluredir={httpfpt_path_config.allure_report_dir}') if allure_clear: run_args.append('--clean-alluredir') @@ -116,9 +119,9 @@ def startup( pytest.main(run_args) log.info('🏁 FINISH') - yaml_report_files = os.listdir(path_config.YAML_REPORT_PATH) + yaml_report_files = os.listdir(httpfpt_path_config.yaml_report_dir) yaml_report_files.sort() - test_result = read_yaml(path_config.YAML_REPORT_PATH, filename=yaml_report_files[-1]) + test_result = read_yaml(httpfpt_path_config.yaml_report_dir, filename=yaml_report_files[-1]) if html_report and config.EMAIL_REPORT_SEND: SendMail(test_result, html_report_filename).send_report() @@ -130,19 +133,19 @@ def startup( LarkTalk(test_result).send() if allure: - if not os.path.exists(path_config.ALLURE_REPORT_ENV_FILE): - shutil.copyfile(path_config.ALLURE_ENV_FILE, path_config.ALLURE_REPORT_ENV_FILE) + if not os.path.exists(httpfpt_path_config.allure_report_env_file): + shutil.copyfile(httpfpt_path_config.allure_env_file, httpfpt_path_config.allure_report_env_file) if allure and allure_serve: os.popen( - f'allure generate {path_config.ALLURE_REPORT_PATH} -o {path_config.ALLURE_REPORT_HTML_PATH} --clean' - ) and os.popen(f'allure serve {path_config.ALLURE_REPORT_PATH}') # type: ignore + f'allure generate {httpfpt_path_config.allure_report_dir} -o {httpfpt_path_config.allure_html_report_dir} ' + + '--clean' + ) and os.popen(f'allure serve {httpfpt_path_config.allure_report_dir}') # type: ignore def run( *args, # init - project_dir: str, settings: str | dict, clean_cache: bool = False, pydantic_verify: bool = True, @@ -168,7 +171,6 @@ def run( 运行入口 :param args: pytest 运行参数 - :param project_dir: 项目根目录 :param settings: 项目核心配置,字典或指定配置文件 :param clean_cache: 清理 redis 缓存数据,对于脏数据,这很有用,默认关闭 :param pydantic_verify: 用例数据完整架构 pydantic 快速检测, 默认开启 @@ -200,7 +202,6 @@ def run( """ log.info(logo) - init_path_config(project_dir) init_config(settings) redis_client.init() case_data.clean_cache_data(clean_cache) diff --git a/httpfpt/utils/auth_plugins.py b/httpfpt/utils/auth_plugins.py index b8116f0..20ddee1 100644 --- a/httpfpt/utils/auth_plugins.py +++ b/httpfpt/utils/auth_plugins.py @@ -8,7 +8,7 @@ from httpfpt.common.errors import AuthError from httpfpt.common.yaml_handler import read_yaml -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config from httpfpt.db.redis_db import redis_client from httpfpt.enums.request.auth import AuthType from httpfpt.utils.enum_control import get_enum_values @@ -24,7 +24,7 @@ def __init__(self) -> None: @lru_cache def get_auth_data(self) -> dict: - auth_data = read_yaml(path_config.AUTH_CONF_PATH, filename='auth.yaml') + auth_data = read_yaml(httpfpt_path_config.auth_conf_dir, filename='auth.yaml') return auth_data @property diff --git a/httpfpt/utils/case_auto_generator.py b/httpfpt/utils/case_auto_generator.py index 23eb71b..b364337 100644 --- a/httpfpt/utils/case_auto_generator.py +++ b/httpfpt/utils/case_auto_generator.py @@ -5,7 +5,7 @@ from pathlib import Path from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config from httpfpt.utils.file_control import get_file_property, search_all_case_data_files, search_all_testcase_files from httpfpt.utils.rich_console import console @@ -92,10 +92,12 @@ def {testcase_func_name}(self, data): new_testcase_filename = testcase_func_name + '.py' if tag: case_path = os.path.join( - path_config.TEST_CASE_PATH, config.PROJECT_NAME, *tag, new_testcase_filename + httpfpt_path_config.testcase_dir, config.PROJECT_NAME, *tag, new_testcase_filename ) else: - case_path = os.path.join(path_config.TEST_CASE_PATH, config.PROJECT_NAME, new_testcase_filename) + case_path = os.path.join( + httpfpt_path_config.testcase_dir, config.PROJECT_NAME, new_testcase_filename + ) new_testcase_dir = Path(case_path).parent if not new_testcase_dir.exists(): new_testcase_dir.mkdir(parents=True, exist_ok=True) diff --git a/httpfpt/utils/data_manage/apifox.py b/httpfpt/utils/data_manage/apifox.py index 0a8262c..60d6332 100644 --- a/httpfpt/utils/data_manage/apifox.py +++ b/httpfpt/utils/data_manage/apifox.py @@ -8,7 +8,7 @@ from httpfpt.common.json_handler import read_json_file from httpfpt.common.yaml_handler import write_yaml from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config from httpfpt.utils.data_manage.base_format import format_value from httpfpt.utils.file_control import get_file_property from httpfpt.utils.rich_console import console @@ -63,7 +63,7 @@ def import_apifox_to_yaml(self, source: str, project: str | None = None) -> None case_config['allure']['feature'] = case_config['module'] = k case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - path_config.CASE_DATA_PATH, + httpfpt_path_config.case_data_dir, os.sep.join([project or config.PROJECT_NAME, k, get_file_property(source)[1] + '.yaml']), case_file_data, mode='w', @@ -73,7 +73,7 @@ def import_apifox_to_yaml(self, source: str, project: str | None = None) -> None for v in root_case.values(): case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - path_config.CASE_DATA_PATH, + httpfpt_path_config.case_data_dir, os.sep.join([project or config.PROJECT_NAME, get_file_property(source)[1] + '.yaml']), case_file_data, mode='w', diff --git a/httpfpt/utils/data_manage/openapi.py b/httpfpt/utils/data_manage/openapi.py index 2f00f12..83130ca 100644 --- a/httpfpt/utils/data_manage/openapi.py +++ b/httpfpt/utils/data_manage/openapi.py @@ -13,7 +13,7 @@ from httpfpt.common.json_handler import read_json_file from httpfpt.common.yaml_handler import write_yaml from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config from httpfpt.utils.data_manage.base_format import format_value from httpfpt.utils.file_control import get_file_property from httpfpt.utils.rich_console import console @@ -183,7 +183,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None case_config['allure']['feature'] = case_config['module'] = k case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - path_config.CASE_DATA_PATH, + httpfpt_path_config.case_data_dir, os.sep.join( [ project or config.PROJECT_NAME, @@ -213,7 +213,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None ) case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - path_config.CASE_DATA_PATH, + httpfpt_path_config.case_data_dir, os.sep.join([project or config.PROJECT_NAME, filename]), case_file_data, mode='w', @@ -239,7 +239,9 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None ) is_write = Confirm.ask(f'❓ 是否需要创建 {tag_filename} 数据文件?', default=True) if is_write: - write_yaml(path_config.CASE_DATA_PATH, tag_filename, case_file_data, mode='w') + write_yaml( + httpfpt_path_config.case_data_dir, tag_filename, case_file_data, mode='w' + ) # 写入项目根目录 if len(root_case) > 0: for k, v in root_case.items(): @@ -259,7 +261,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None if is_write: case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - path_config.CASE_DATA_PATH, + httpfpt_path_config.case_data_dir, os.sep.join([project or config.PROJECT_NAME, root_filename]), case_file_data, mode='w', diff --git a/httpfpt/utils/file_control.py b/httpfpt/utils/file_control.py index 312d4b8..be7f3eb 100644 --- a/httpfpt/utils/file_control.py +++ b/httpfpt/utils/file_control.py @@ -8,7 +8,7 @@ from pathlib import Path from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config def get_file_property(filepath: str) -> tuple[str, str, str]: @@ -32,7 +32,7 @@ def search_all_case_data_files(filepath: str | None = None) -> list: :return: """ case_data_filepath = ( - os.path.join(path_config.CASE_DATA_PATH, f'{config.PROJECT_NAME}') if filepath is None else filepath + os.path.join(httpfpt_path_config.case_data_dir, f'{config.PROJECT_NAME}') if filepath is None else filepath ) files = ( glob.glob(os.path.join(case_data_filepath, '**', '*.yaml'), recursive=True) @@ -49,7 +49,7 @@ def search_all_testcase_files() -> list: :return: """ files = glob.glob( - os.path.join(path_config.TEST_CASE_PATH, f'{config.PROJECT_NAME}', '**', 'test_*.py'), recursive=True + os.path.join(httpfpt_path_config.testcase_dir, f'{config.PROJECT_NAME}', '**', 'test_*.py'), recursive=True ) return files diff --git a/httpfpt/utils/request/request_data_parse.py b/httpfpt/utils/request/request_data_parse.py index 803bf4c..1eea1e4 100644 --- a/httpfpt/utils/request/request_data_parse.py +++ b/httpfpt/utils/request/request_data_parse.py @@ -15,7 +15,7 @@ from httpfpt.common.env_handler import get_env_dict from httpfpt.common.errors import RequestDataParseError from httpfpt.common.log import log -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config from httpfpt.db.mysql_db import mysql_client from httpfpt.enums.allure_severity_type import SeverityType from httpfpt.enums.request.auth import AuthType @@ -331,7 +331,7 @@ def url(self) -> str: if not url.startswith('http'): _env = self.env try: - env_file = os.path.join(path_config.RUN_ENV_PATH, _env) + env_file = os.path.join(httpfpt_path_config.run_env_dir, _env) env_dict = get_env_dict(env_file) except Exception as e: raise RequestDataParseError(f'环境变量 {_env} 读取失败: {e}') diff --git a/httpfpt/utils/request/vars_extractor.py b/httpfpt/utils/request/vars_extractor.py index 0a09f7a..0f5b1ff 100644 --- a/httpfpt/utils/request/vars_extractor.py +++ b/httpfpt/utils/request/vars_extractor.py @@ -13,7 +13,7 @@ from httpfpt.common.log import log from httpfpt.common.variable_cache import variable_cache from httpfpt.common.yaml_handler import read_yaml, write_yaml_vars -from httpfpt.core.path_conf import path_config +from httpfpt.core.path_conf import httpfpt_path_config from httpfpt.enums.var_type import VarType @@ -48,13 +48,13 @@ def vars_replace(self, target: dict, env_filename: str | None = None) -> dict: if not env or not isinstance(env, str): raise RequestDataParseError('运行环境获取失败, 测试用例数据缺少 config:request:env 参数') try: - env_file = os.path.join(path_config.RUN_ENV_PATH, env) + env_file = os.path.join(httpfpt_path_config.run_env_dir, env) env_vars = get_env_dict(env_file) except OSError: raise RequestDataParseError('运行环境获取失败, 请检查测试用例环境配置') # 获取全局变量 - global_vars = read_yaml(path_config.TEST_DATA_PATH, filename='global_vars.yaml') + global_vars = read_yaml(httpfpt_path_config.data_path, filename='global_vars.yaml') # 获取 re 规则字符串 var_re = self.sql_vars_re if env_filename else self.vars_re @@ -149,7 +149,7 @@ def teardown_var_extract(response: dict, extract: dict, env: str) -> None: if set_type == VarType.CACHE: variable_cache.set(key, value_str) elif set_type == VarType.ENV: - write_env_vars(path_config.RUN_ENV_PATH, env, key, value_str) + write_env_vars(httpfpt_path_config.run_env_dir, env, key, value_str) elif set_type == VarType.GLOBAL: write_yaml_vars({key: value_str}) else: From 59e536370fc8bbb96b8590f3cc069d6dcfd24dbc Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 4 Mar 2024 15:53:54 +0800 Subject: [PATCH 09/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/core/path_conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index 384cbd3..d7ca86f 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -106,7 +106,10 @@ def set_project_dir(base_dir: str) -> HttpFptPathConfig: :param base_dir: 项目根路径 :return: """ - return HttpFptPathConfig(base_dir) + global httpfpt_path_config + + httpfpt_path_config = HttpFptPathConfig(base_dir) + return httpfpt_path_config # global path_config From 2a6043d53b97482f45250ef368e56bd1b75db41a Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 4 Mar 2024 18:53:03 +0800 Subject: [PATCH 10/46] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8F=98=E9=87=8F=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 34 ++----- httpfpt/common/json_handler.py | 9 +- httpfpt/core/get_conf.py | 158 +++++++++++++++++++-------------- httpfpt/run.py | 5 +- pyrightconfig.json | 6 +- 5 files changed, 102 insertions(+), 110 deletions(-) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index e4fc60c..adff430 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -180,23 +180,20 @@ def create_new_project(start_project: tuple[str, str]) -> None: shutil.copyfile('conftest.py', project_path) shutil.copyfile('pytest.ini', project_path) run_settings_path = os.path.join(project_path, 'core', 'conf.toml') - init_tpl = """#!/usr/bin/env python3 + init_tpl = f"""#!/usr/bin/env python3 # -*- coding: utf-8 -*- -from pathlib import Path - from httpfpt import set_project_dir +from httpfpt import set_project_config - -BASE_DIR = Path(__file__).resolve().parent -set_project_dir(BASE_DIR) +set_project_dir({project_path}) +set_project_config({run_settings_path}) """ - run_tpl = f"""#!/usr/bin/env python3 + run_tpl = """#!/usr/bin/env python3 # -*- coding: utf-8 -*- from httpfpt import run - -run(settings={run_settings_path}, path_dir=str(BASE_DIR)) +run() """ with open(os.path.join(project_path, '__init__.py'), 'w', encoding='utf-8') as f: f.write(init_tpl) @@ -227,20 +224,6 @@ class HttpFptCLI: required=False, ), ] - # run_test: Annotated[ - # list[str] | None, - # cappa.Arg( - # value_name='', - # short='-r', - # long='--run', - # default=None, - # help='Run test cases, do not support use with other commands, - # but support custom pytest running parameters,' - # ' default parameters see `httpfpt/run.py`.', - # parse=cmd_run_test_parse, - # num_args=-1, - # ), - # ] subcmd: Subcommands[TestCaseCLI | ImportCLI | None] = None def __call__(self) -> None: @@ -248,11 +231,6 @@ def __call__(self) -> None: get_version() if self.start_project: create_new_project(self.start_project) - # if self.run_test: - # if self.version or self.subcmd: - # console.print('\n❌ 暂不支持 -r/--run 命令与其他 CLI 命令同时使用') - # raise cappa.Exit(code=1) - # run(*self.run_test) if isinstance(self.run_test, list) else run() @cappa.command(name='testcase', help='Test case tools.') diff --git a/httpfpt/common/json_handler.py b/httpfpt/common/json_handler.py index 5f6da19..78db6ee 100644 --- a/httpfpt/common/json_handler.py +++ b/httpfpt/common/json_handler.py @@ -21,14 +21,9 @@ def read_json_file(filepath: str | None = httpfpt_path_config.case_data_dir, *, :return: """ if filepath is not None: - _file = os.path.join(filepath, filename) - else: - _file = filename - if not _file: - log.error('读取 yaml 文件失败,文件名为空') - raise FileNotFoundError('读取 yaml 文件失败,文件名为空') + filepath = os.path.join(filepath, filename) try: - with open(_file, encoding='utf-8') as f: + with open(filepath, encoding='utf-8') as f: # type: ignore data = json.load(f, **kwargs) except Exception as e: log.error(f'文件 {filename} 读取错误: {e}') diff --git a/httpfpt/core/get_conf.py b/httpfpt/core/get_conf.py index c33cf53..16b3dbc 100644 --- a/httpfpt/core/get_conf.py +++ b/httpfpt/core/get_conf.py @@ -2,9 +2,14 @@ # -*- coding: utf-8 -*- from __future__ import annotations -from httpfpt.common.toml_handler import read_toml +import os.path -__all__ = ['config', 'init_config'] +from httpfpt.common.errors import ConfigInitError + +__all__ = [ + 'config', + 'set_project_config', +] # global config config = None @@ -17,84 +22,101 @@ def __init__(self, settings: dict) -> None: :param settings: """ - # 项目目录名 - self.PROJECT_NAME = settings['project']['project'] - # 测试报告 - self.TEST_REPORT_TITLE = settings['report']['title'] - self.TESTER_NAME = settings['report']['tester_name'] - # mysql 数据库 - self.MYSQL_HOST = settings['mysql']['host'] - self.MYSQL_PORT = settings['mysql']['port'] - self.MYSQL_USER = settings['mysql']['user'] - self.MYSQL_PASSWORD = settings['mysql']['password'] - self.MYSQL_DATABASE = settings['mysql']['database'] - self.MYSQL_CHARSET = settings['mysql']['charset'] - # redis 数据库 - self.REDIS_HOST = settings['redis']['host'] - self.REDIS_PORT = settings['redis']['port'] - self.REDIS_PASSWORD = settings['redis']['password'] - self.REDIS_DATABASE = settings['redis']['database'] - self.REDIS_TIMEOUT = settings['redis']['timeout'] - # 邮件 - self.EMAIL_SERVER = settings['email']['host_server'] - self.EMAIL_PORT = settings['email']['port'] - self.EMAIL_USER = settings['email']['user'] - self.EMAIL_PASSWORD = settings['email']['password'] - self.EMAIL_SEND_TO = settings['email']['send_to'] - self.EMAIL_SSL = settings['email']['ssl'] - self.EMAIL_REPORT_SEND = settings['email']['send_report'] - # 钉钉 - self.DING_TALK_WEBHOOK = settings['ding_talk']['webhook'] - self.DING_TALK_PROXY = { - 'http': settings['ding_talk']['proxies']['http'] - if settings['ding_talk']['proxies']['http'] != '' - else None, - 'https': settings['ding_talk']['proxies']['https'] - if settings['ding_talk']['proxies']['https'] != '' - else None, - } - self.DING_TALK_REPORT_SEND = settings['ding_talk']['send_report'] - # 飞书 - self.LARK_TALK_WEBHOOK = settings['lark_talk']['webhook'] - self.LARK_TALK_PROXY = { - 'http': settings['lark_talk']['proxies']['http'] - if settings['lark_talk']['proxies']['http'] != '' - else None, - 'https': settings['lark_talk']['proxies']['https'] - if settings['lark_talk']['proxies']['https'] != '' - else None, - } - self.LARK_TALK_REPORT_SEND = settings['lark_talk']['send_report'] - # 请求发送 - self.REQUEST_TIMEOUT = settings['request']['timeout'] - self.REQUEST_VERIFY = settings['request']['verify'] - self.REQUEST_REDIRECTS = settings['request']['redirects'] - self.REQUEST_PROXIES_REQUESTS = { - 'http': settings['request']['proxies']['http'] if settings['request']['proxies']['http'] != '' else None, - 'https': settings['request']['proxies']['https'] if settings['request']['proxies']['https'] != '' else None, - } - self.REQUEST_PROXIES_HTTPX = { - 'http://': settings['request']['proxies']['http'] if settings['request']['proxies']['http'] != '' else None, - 'https://': settings['request']['proxies']['https'] - if settings['request']['proxies']['https'] != '' - else None, - } - self.REQUEST_RETRY = settings['request']['retry'] + try: + # 项目目录名 + self.PROJECT_NAME = settings['project']['project'] + # 测试报告 + self.TEST_REPORT_TITLE = settings['report']['title'] + self.TESTER_NAME = settings['report']['tester_name'] + # mysql 数据库 + self.MYSQL_HOST = settings['mysql']['host'] + self.MYSQL_PORT = settings['mysql']['port'] + self.MYSQL_USER = settings['mysql']['user'] + self.MYSQL_PASSWORD = settings['mysql']['password'] + self.MYSQL_DATABASE = settings['mysql']['database'] + self.MYSQL_CHARSET = settings['mysql']['charset'] + # redis 数据库 + self.REDIS_HOST = settings['redis']['host'] + self.REDIS_PORT = settings['redis']['port'] + self.REDIS_PASSWORD = settings['redis']['password'] + self.REDIS_DATABASE = settings['redis']['database'] + self.REDIS_TIMEOUT = settings['redis']['timeout'] + # 邮件 + self.EMAIL_SERVER = settings['email']['host_server'] + self.EMAIL_PORT = settings['email']['port'] + self.EMAIL_USER = settings['email']['user'] + self.EMAIL_PASSWORD = settings['email']['password'] + self.EMAIL_SEND_TO = settings['email']['send_to'] + self.EMAIL_SSL = settings['email']['ssl'] + self.EMAIL_REPORT_SEND = settings['email']['send_report'] + # 钉钉 + self.DING_TALK_WEBHOOK = settings['ding_talk']['webhook'] + self.DING_TALK_PROXY = { + 'http': settings['ding_talk']['proxies']['http'] + if settings['ding_talk']['proxies']['http'] != '' + else None, + 'https': settings['ding_talk']['proxies']['https'] + if settings['ding_talk']['proxies']['https'] != '' + else None, + } + self.DING_TALK_REPORT_SEND = settings['ding_talk']['send_report'] + # 飞书 + self.LARK_TALK_WEBHOOK = settings['lark_talk']['webhook'] + self.LARK_TALK_PROXY = { + 'http': settings['lark_talk']['proxies']['http'] + if settings['lark_talk']['proxies']['http'] != '' + else None, + 'https': settings['lark_talk']['proxies']['https'] + if settings['lark_talk']['proxies']['https'] != '' + else None, + } + self.LARK_TALK_REPORT_SEND = settings['lark_talk']['send_report'] + # 请求发送 + self.REQUEST_TIMEOUT = settings['request']['timeout'] + self.REQUEST_VERIFY = settings['request']['verify'] + self.REQUEST_REDIRECTS = settings['request']['redirects'] + self.REQUEST_PROXIES_REQUESTS = { + 'http': settings['request']['proxies']['http'] + if settings['request']['proxies']['http'] != '' + else None, + 'https': settings['request']['proxies']['https'] + if settings['request']['proxies']['https'] != '' + else None, + } + self.REQUEST_PROXIES_HTTPX = { + 'http://': settings['request']['proxies']['http'] + if settings['request']['proxies']['http'] != '' + else None, + 'https://': settings['request']['proxies']['https'] + if settings['request']['proxies']['https'] != '' + else None, + } + self.REQUEST_RETRY = settings['request']['retry'] + except KeyError as e: + raise ConfigInitError( + f'配置文件解析失败:缺失参数 "{str(e)}"。请核对配置文件或字典。若尚未配置,可通过' + f'set_project_config() 设置项目配置,或将 HTTPFPT_PROJECT_CONFIG 环境变量指向配置文件路径' + ) -def init_config(settings: str | dict, config_filename: str | None = None) -> None: +def set_project_config(settings: str | dict, config_filename: str | None = None) -> HttpFptConfig: """ - 初始化配置 + 设置项目配置 - :param settings: 项目配置,字典或指定 toml 配置路径文件 + :param settings: 项目配置,字典或指定 toml 配置文件 :param config_filename: :return: """ global config if isinstance(settings, str): + from httpfpt.common.toml_handler import read_toml + + if not os.path.isdir(settings) and not os.path.isfile(settings): + raise ConfigInitError('运行失败,') conf = read_toml(settings, config_filename) else: conf = settings config = HttpFptConfig(conf) + return config diff --git a/httpfpt/run.py b/httpfpt/run.py index 5a55546..11eb83f 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -14,7 +14,7 @@ from httpfpt.common.log import log from httpfpt.common.yaml_handler import read_yaml -from httpfpt.core.get_conf import config, init_config +from httpfpt.core.get_conf import config from httpfpt.core.path_conf import httpfpt_path_config from httpfpt.db.redis_db import redis_client from httpfpt.utils.request import case_data_parse as case_data @@ -146,7 +146,6 @@ def startup( def run( *args, # init - settings: str | dict, clean_cache: bool = False, pydantic_verify: bool = True, # log level @@ -171,7 +170,6 @@ def run( 运行入口 :param args: pytest 运行参数 - :param settings: 项目核心配置,字典或指定配置文件 :param clean_cache: 清理 redis 缓存数据,对于脏数据,这很有用,默认关闭 :param pydantic_verify: 用例数据完整架构 pydantic 快速检测, 默认开启 :param args: pytest 运行参数 @@ -202,7 +200,6 @@ def run( """ log.info(logo) - init_config(settings) redis_client.init() case_data.clean_cache_data(clean_cache) case_data.case_data_init(pydantic_verify) diff --git a/pyrightconfig.json b/pyrightconfig.json index 7eb1a65..089948d 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -4,11 +4,11 @@ "**/.*", "venv/**", "httpfpt/testcases/**", - "httpfpt/utils/data_manage/**.py" + "httpfpt/utils/data_manage/**.py", + "httpfpt/conftest.py" ], "ignore": [ - "httpfpt/utils/auth_plugins.py", - "httpfpt/conftest.py" + "httpfpt/utils/auth_plugins.py" ], "reportUnusedImport": "error", "reportOptionalMemberAccess": false, From 1957c1d95ad251ba88d860029be930cf9942edeb Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 4 Mar 2024 19:09:19 +0800 Subject: [PATCH 11/46] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/__init__.py | 2 ++ pyproject.toml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/httpfpt/__init__.py b/httpfpt/__init__.py index c41df8a..bef887c 100644 --- a/httpfpt/__init__.py +++ b/httpfpt/__init__.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from httpfpt.core.get_conf import set_project_config as set_project_config +from httpfpt.core.path_conf import set_project_dir as set_project_dir __version__ = 'v0.5.1' diff --git a/pyproject.toml b/pyproject.toml index 91e4228..e16dd5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,9 @@ test = [ "ruff>=0.2.0", ] +[tool.pdm.scripts] +httpfpt = { cmd = "python cli.py" } + [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" From 1a0980a93db83d91221e019150253739f9ecd178 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 4 Mar 2024 21:56:46 +0800 Subject: [PATCH 12/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0cli=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- httpfpt/__init__.py | 1 + httpfpt/cli.py | 203 +++----------------------- httpfpt/utils/cli/__init__.py | 2 + httpfpt/utils/cli/about_testcase.py | 69 +++++++++ httpfpt/utils/cli/import_case_data.py | 67 +++++++++ httpfpt/utils/cli/new_project.py | 45 ++++++ httpfpt/utils/cli/version.py | 17 +++ httpfpt/utils/enum_control.py | 7 +- pyproject.toml | 4 +- 10 files changed, 227 insertions(+), 190 deletions(-) create mode 100644 httpfpt/utils/cli/__init__.py create mode 100644 httpfpt/utils/cli/about_testcase.py create mode 100644 httpfpt/utils/cli/import_case_data.py create mode 100644 httpfpt/utils/cli/new_project.py create mode 100644 httpfpt/utils/cli/version.py diff --git a/.gitignore b/.gitignore index cb9dd35..bf9f2c7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ httpfpt/report/ httpfpt/log/ .idea/ httpfpt/data/online* -httpfpt/.ruff_cache/ +.ruff_cache/ diff --git a/httpfpt/__init__.py b/httpfpt/__init__.py index bef887c..7c695e0 100644 --- a/httpfpt/__init__.py +++ b/httpfpt/__init__.py @@ -2,5 +2,6 @@ # -*- coding: utf-8 -*- from httpfpt.core.get_conf import set_project_config as set_project_config from httpfpt.core.path_conf import set_project_dir as set_project_dir +from httpfpt.run import run as run __version__ = 'v0.5.1' diff --git a/httpfpt/cli.py b/httpfpt/cli.py index adff430..a4ae74d 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -3,202 +3,28 @@ from __future__ import annotations import os -import re -import shutil import sys from dataclasses import dataclass -from typing import TYPE_CHECKING import cappa from cappa import Subcommands -from pydantic import ValidationError -from rich.prompt import Confirm from typing_extensions import Annotated sys.path.append(os.path.dirname(os.path.dirname(__file__))) -from httpfpt.common.json_handler import read_json_file -from httpfpt.common.yaml_handler import read_yaml -from httpfpt.enums.case_data_type import CaseDataType -from httpfpt.schemas.case_data import CaseData -from httpfpt.utils.case_auto_generator import auto_generate_testcases -from httpfpt.utils.data_manage.apifox import ApiFoxParser -from httpfpt.utils.data_manage.git_repo import GitRepoPaser -from httpfpt.utils.data_manage.openapi import SwaggerParser -from httpfpt.utils.file_control import get_file_property, search_all_case_data_files -from httpfpt.utils.rich_console import console - -if TYPE_CHECKING: - from cappa.parser import Value - - -def get_version() -> None: - """获取版本号""" - ver = open('./__init__.py', 'rt').read() - mob = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", ver, re.MULTILINE) - if mob: - console.print('\n🔥 HttpFpt', mob.group(1)) - else: - raise cappa.Exit('未查询到版本号', code=1) - - -def testcase_data_verify(verify: str) -> None: - """测试数据验证""" - msg: str = '' - try: - count: int = 0 - if verify.lower() == 'all': - console.print('\n🔥 开始验证所有测试数据结构...') - file_list = search_all_case_data_files() - for file in file_list: - file_type = get_file_property(file)[2] - if file_type == CaseDataType.JSON: - file_data = read_json_file(None, filename=file) - else: - file_data = read_yaml(None, filename=file) - CaseData.model_validate(file_data) - else: - console.print(f'🔥 开始验证 {verify} 测试数据结构...') - file_data = read_yaml(None, filename=verify) - CaseData.model_validate(file_data) - except ValidationError as e: - count = e.error_count() - msg += str(e) - except Exception as e: - console.print(f'❌ 验证测试数据 {verify} 结构失败: {e}') - raise cappa.Exit(code=1) - if count > 0: - console.print(f'❌ 验证测试数据 {verify} 结构失败: {msg}') - raise cappa.Exit(code=1) - else: - console.print('✅ 验证测试数据结构成功') - - -def generate_testcases() -> None: - """生成测试用例""" - console.print( - '\n' - 'Warning: 此操作生成的测试用例是依赖测试数据文件而决定的,\n' - ' 如果你手动创建的测试用例与测试数据文件名称相吻合,\n' - ' 那么此操作将不能完全保证你的手动创建测试用例继续保留,\n' - ' 如果你依然执行此操作, 请谨慎选择重新生成所有测试用例。\n', - style='bold #ffd700', - ) - result = Confirm.ask('⚠️ 是否重新生成所有测试用例?', default=False) - try: - if result: - console.print('🔥 开始重新生成所有测试用例...') - auto_generate_testcases(rewrite=True) - else: - console.print('🔥 开始生成新测试用例...') - auto_generate_testcases() - except Exception as e: - console.print(f'❌ 自动生成测试用例失败: {e}') - raise cappa.Exit(code=1) - - -def import_openapi_case_data(openapi: tuple[str, str]) -> None: - """导入 openapi 测试用例数据""" - console.print(f'\n📩 正在导入测试用例数据到项目: [#0087ff]{openapi[1]}[/#0087ff]') - result = Confirm.ask('❓ 确认执行此操作吗?', default=False) - if result: - console.print('🔥 开始导入 openapi 数据...') - try: - SwaggerParser().import_openapi_to_yaml(openapi[0], openapi[1]) - except Exception as e: - console.print('❌ 导入 openapi 数据失败') - raise e - - -def import_apifox_case_data(apifox: tuple[str, str]) -> None: - """导入 apifox 测试用例数据""" - console.print( - '\n' - 'Beta: 此命令目前处于测试阶段, 请谨慎使用。\n' - 'Warning: 如果现有文件名与导入文件名相同, 此命令目前会覆盖写入用例数据, 请谨慎操作。\n', - style='bold #ffd700', - ) - result = Confirm.ask('⚠️ 确认执行此操作吗?', default=False) - if result: - console.print('🔥 开始导入 apifox 数据...') - try: - ApiFoxParser().import_apifox_to_yaml(apifox[0], apifox[1]) - except Exception as e: - console.print('❌ 导入 apifox 数据失败:') - raise e - - -def import_har_case_data(har: tuple[str, str]) -> None: - """导入 har 测试用例数据""" - console.print('\n🚧 此功能暂未开发') - - -def import_jmeter_case_data(jmeter: tuple[str, str]) -> None: - """导入 jmeter 测试用例数据""" - console.print('\n🚧 此功能暂未开发') - - -def import_postman_case_data(postman: tuple[str, str]) -> None: - """导入 postman 测试用例数据""" - console.print('\n🚧 此功能暂未开发') - - -def import_git_case_data(src: str) -> None: - """导入 git 仓库测试数据""" - console.print(f'\n🚀 正在导入 git 仓库测试数据到本地: {src}') - console.print('🔥 开始导入 git 仓库测试数据...\n') - try: - GitRepoPaser.import_git_to_local(src) - except Exception as e: - console.print(f'❌ 导入 git 仓库测试数据失败: {e}') - raise e - - -def cmd_run_test_parse(value: Value) -> bool | Value: - """运行测试命令参数解析""" - if len(value) == 0: # type: ignore - return True - else: - return value - - -def create_new_project(start_project: tuple[str, str]) -> None: - name = start_project[0] - path = start_project[1] - if path != '.': - if not os.path.isdir(path): - raise cappa.Exit(f'"{path}" is not a directory', code=1) - project_path = os.path.join(path, name) - if os.path.exists(project_path): - raise cappa.Exit('The project directory is not empty', code=1) - os.makedirs(project_path) - shutil.copytree('./core', project_path, ignore=shutil.ignore_patterns('get_conf.py', 'path_conf.py')) - shutil.copytree('./data', project_path) - shutil.copytree('./testcases', project_path) - shutil.copyfile('conftest.py', project_path) - shutil.copyfile('pytest.ini', project_path) - run_settings_path = os.path.join(project_path, 'core', 'conf.toml') - init_tpl = f"""#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from httpfpt import set_project_dir -from httpfpt import set_project_config - -set_project_dir({project_path}) -set_project_config({run_settings_path}) -""" - run_tpl = """#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from httpfpt import run - - -run() -""" - with open(os.path.join(project_path, '__init__.py'), 'w', encoding='utf-8') as f: - f.write(init_tpl) - with open(os.path.join(project_path, 'run.py'), 'w', encoding='utf-8') as f: - f.write(run_tpl) +from httpfpt.utils.cli.about_testcase import generate_testcases, testcase_data_verify +from httpfpt.utils.cli.import_case_data import ( + import_apifox_case_data, + import_git_case_data, + import_har_case_data, + import_jmeter_case_data, + import_openapi_case_data, + import_postman_case_data, +) +from httpfpt.utils.cli.new_project import create_new_project +from httpfpt.utils.cli.version import get_version @cappa.command(name='httpfpt-cli') @@ -347,5 +173,10 @@ def __call__(self) -> None: import_git_case_data(self.git) -if __name__ == '__main__': +def cappa_invoke() -> None: + """cli 执行程序""" cappa.invoke(HttpFptCLI) + + +if __name__ == '__main__': + cappa_invoke() diff --git a/httpfpt/utils/cli/__init__.py b/httpfpt/utils/cli/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/httpfpt/utils/cli/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/httpfpt/utils/cli/about_testcase.py b/httpfpt/utils/cli/about_testcase.py new file mode 100644 index 0000000..bea71a7 --- /dev/null +++ b/httpfpt/utils/cli/about_testcase.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import cappa + +from pydantic import ValidationError +from rich.prompt import Confirm + +from httpfpt.common.json_handler import read_json_file +from httpfpt.common.yaml_handler import read_yaml +from httpfpt.enums.case_data_type import CaseDataType +from httpfpt.schemas.case_data import CaseData +from httpfpt.utils.case_auto_generator import auto_generate_testcases +from httpfpt.utils.file_control import get_file_property, search_all_case_data_files +from httpfpt.utils.rich_console import console + + +def testcase_data_verify(verify: str) -> None: + """测试数据验证""" + msg: str = '' + try: + count: int = 0 + if verify.lower() == 'all': + console.print('\n🔥 开始验证所有测试数据结构...') + file_list = search_all_case_data_files() + for file in file_list: + file_type = get_file_property(file)[2] + if file_type == CaseDataType.JSON: + file_data = read_json_file(None, filename=file) + else: + file_data = read_yaml(None, filename=file) + CaseData.model_validate(file_data) + else: + console.print(f'🔥 开始验证 {verify} 测试数据结构...') + file_data = read_yaml(None, filename=verify) + CaseData.model_validate(file_data) + except ValidationError as e: + count = e.error_count() + msg += str(e) + except Exception as e: + console.print(f'❌ 验证测试数据 {verify} 结构失败: {e}') + raise cappa.Exit(code=1) + if count > 0: + console.print(f'❌ 验证测试数据 {verify} 结构失败: {msg}') + raise cappa.Exit(code=1) + else: + console.print('✅ 验证测试数据结构成功') + + +def generate_testcases() -> None: + """生成测试用例""" + console.print( + '\n' + 'Warning: 此操作生成的测试用例是依赖测试数据文件而决定的,\n' + ' 如果你手动创建的测试用例与测试数据文件名称相吻合,\n' + ' 那么此操作将不能完全保证你的手动创建测试用例继续保留,\n' + ' 如果你依然执行此操作, 请谨慎选择重新生成所有测试用例。\n', + style='bold #ffd700', + ) + result = Confirm.ask('⚠️ 是否重新生成所有测试用例?', default=False) + try: + if result: + console.print('🔥 开始重新生成所有测试用例...') + auto_generate_testcases(rewrite=True) + else: + console.print('🔥 开始生成新测试用例...') + auto_generate_testcases() + except Exception as e: + console.print(f'❌ 自动生成测试用例失败: {e}') + raise cappa.Exit(code=1) diff --git a/httpfpt/utils/cli/import_case_data.py b/httpfpt/utils/cli/import_case_data.py new file mode 100644 index 0000000..64bb85b --- /dev/null +++ b/httpfpt/utils/cli/import_case_data.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from __future__ import annotations + +from rich.prompt import Confirm + +from httpfpt.utils.data_manage.apifox import ApiFoxParser +from httpfpt.utils.data_manage.git_repo import GitRepoPaser +from httpfpt.utils.data_manage.openapi import SwaggerParser +from httpfpt.utils.rich_console import console + + +def import_openapi_case_data(openapi: tuple[str, str]) -> None: + """导入 openapi 测试用例数据""" + console.print(f'\n📩 正在导入测试用例数据到项目: [#0087ff]{openapi[1]}[/#0087ff]') + result = Confirm.ask('❓ 确认执行此操作吗?', default=False) + if result: + console.print('🔥 开始导入 openapi 数据...') + try: + SwaggerParser().import_openapi_to_yaml(openapi[0], openapi[1]) + except Exception as e: + console.print('❌ 导入 openapi 数据失败') + raise e + + +def import_apifox_case_data(apifox: tuple[str, str]) -> None: + """导入 apifox 测试用例数据""" + console.print( + '\n' + 'Beta: 此命令目前处于测试阶段, 请谨慎使用。\n' + 'Warning: 如果现有文件名与导入文件名相同, 此命令目前会覆盖写入用例数据, 请谨慎操作。\n', + style='bold #ffd700', + ) + result = Confirm.ask('⚠️ 确认执行此操作吗?', default=False) + if result: + console.print('🔥 开始导入 apifox 数据...') + try: + ApiFoxParser().import_apifox_to_yaml(apifox[0], apifox[1]) + except Exception as e: + console.print('❌ 导入 apifox 数据失败:') + raise e + + +def import_har_case_data(har: tuple[str, str]) -> None: + """导入 har 测试用例数据""" + console.print('\n🚧 此功能暂未开发') + + +def import_jmeter_case_data(jmeter: tuple[str, str]) -> None: + """导入 jmeter 测试用例数据""" + console.print('\n🚧 此功能暂未开发') + + +def import_postman_case_data(postman: tuple[str, str]) -> None: + """导入 postman 测试用例数据""" + console.print('\n🚧 此功能暂未开发') + + +def import_git_case_data(src: str) -> None: + """导入 git 仓库测试数据""" + console.print(f'\n🚀 正在导入 git 仓库测试数据到本地: {src}') + console.print('🔥 开始导入 git 仓库测试数据...\n') + try: + GitRepoPaser.import_git_to_local(src) + except Exception as e: + console.print(f'❌ 导入 git 仓库测试数据失败: {e}') + raise e diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py new file mode 100644 index 0000000..f0884fb --- /dev/null +++ b/httpfpt/utils/cli/new_project.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from __future__ import annotations + +import os +import shutil + +import cappa + + +def create_new_project(start_project: tuple[str, str]) -> None: + name = start_project[0] + path = start_project[1] + if path != '.': + if not os.path.isdir(path): + raise cappa.Exit(f'"{path}" is not a directory', code=1) + project_path = os.path.join(path, name) + if os.path.exists(project_path): + raise cappa.Exit('The project directory is not empty', code=1) + os.makedirs(project_path) + shutil.copytree('./core', project_path, ignore=shutil.ignore_patterns('get_conf.py', 'path_conf.py')) + shutil.copytree('./data', project_path) + shutil.copytree('./testcases', project_path) + shutil.copyfile('conftest.py', project_path) + shutil.copyfile('pytest.ini', project_path) + run_settings_path = os.path.join(project_path, 'core', 'conf.toml') + init_tpl = f"""#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from httpfpt import set_project_dir +from httpfpt import set_project_config + +set_project_dir({project_path}) +set_project_config({run_settings_path}) +""" + run_tpl = """#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from httpfpt import run + + +run() +""" + with open(os.path.join(project_path, '__init__.py'), 'w', encoding='utf-8') as f: + f.write(init_tpl) + with open(os.path.join(project_path, 'run.py'), 'w', encoding='utf-8') as f: + f.write(run_tpl) diff --git a/httpfpt/utils/cli/version.py b/httpfpt/utils/cli/version.py new file mode 100644 index 0000000..7d176e6 --- /dev/null +++ b/httpfpt/utils/cli/version.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import re + +import cappa + +from httpfpt.utils.rich_console import console + + +def get_version() -> None: + """获取版本号""" + ver = open('./__init__.py', 'rt').read() + mob = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", ver, re.MULTILINE) + if mob: + console.print('\n🔥 HttpFpt', mob.group(1)) + else: + raise cappa.Exit('未查询到版本号', code=1) diff --git a/httpfpt/utils/enum_control.py b/httpfpt/utils/enum_control.py index 3e7357e..3f0c7a8 100644 --- a/httpfpt/utils/enum_control.py +++ b/httpfpt/utils/enum_control.py @@ -1,6 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from enum import Enum +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from enum import Enum def get_enum_values(enum_class: type[Enum]) -> list: diff --git a/pyproject.toml b/pyproject.toml index e16dd5a..62bb977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,8 +51,8 @@ test = [ "ruff>=0.2.0", ] -[tool.pdm.scripts] -httpfpt = { cmd = "python cli.py" } +[project.scripts] +httpfpt = "httpfpt.cli:cappa_invoke" [build-system] requires = ["pdm-backend"] From 03ced5537154592750cf3247857c8e3413a01d09 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 4 Mar 2024 21:58:09 +0800 Subject: [PATCH 13/46] =?UTF-8?q?=E9=BB=98=E8=AE=A4=E4=B8=8D=E5=AF=BC?= =?UTF-8?q?=E5=87=BArun=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/__init__.py | 1 - httpfpt/utils/cli/new_project.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/httpfpt/__init__.py b/httpfpt/__init__.py index 7c695e0..bef887c 100644 --- a/httpfpt/__init__.py +++ b/httpfpt/__init__.py @@ -2,6 +2,5 @@ # -*- coding: utf-8 -*- from httpfpt.core.get_conf import set_project_config as set_project_config from httpfpt.core.path_conf import set_project_dir as set_project_dir -from httpfpt.run import run as run __version__ = 'v0.5.1' diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index f0884fb..09b61a4 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -34,10 +34,10 @@ def create_new_project(start_project: tuple[str, str]) -> None: """ run_tpl = """#!/usr/bin/env python3 # -*- coding: utf-8 -*- -from httpfpt import run +from httpfpt.run import run as httpfpt_run -run() +httpfpt_run() """ with open(os.path.join(project_path, '__init__.py'), 'w', encoding='utf-8') as f: f.write(init_tpl) From 47e0d127bcce7b58c97194e4b90dfa1b2170490d Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 4 Mar 2024 22:11:09 +0800 Subject: [PATCH 14/46] =?UTF-8?q?=E5=88=A0=E9=99=A4=E9=81=97=E7=95=99?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- copier.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 copier.yaml diff --git a/copier.yaml b/copier.yaml deleted file mode 100644 index e69de29..0000000 From 6109b4b2bcd7b380d34de05baa899e512ae40921 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 4 Mar 2024 22:38:52 +0800 Subject: [PATCH 15/46] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E8=B7=AF=E5=BE=84cli=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index a4ae74d..ac8aa46 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -14,6 +14,7 @@ sys.path.append(os.path.dirname(os.path.dirname(__file__))) +from httpfpt import set_project_dir from httpfpt.utils.cli.about_testcase import generate_testcases, testcase_data_verify from httpfpt.utils.cli.import_case_data import ( import_apifox_case_data, @@ -39,6 +40,17 @@ class HttpFptCLI: help='Print version information.', ), ] + project_dir: Annotated[ + str, + cappa.Arg( + value_name='', + short=True, + long=True, + default='.', + help='Set the project directory path.', + required=False, + ), + ] start_project: Annotated[ tuple[str, str], cappa.Arg( @@ -55,6 +67,8 @@ class HttpFptCLI: def __call__(self) -> None: if self.version: get_version() + if self.project_dir: + set_project_dir(self.project_dir) if self.start_project: create_new_project(self.start_project) From 2be5dac0c4d17587d4d3ecaab892f4f8791668cc Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Wed, 6 Mar 2024 11:09:04 +0800 Subject: [PATCH 16/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E5=91=BD=E5=90=8D=E5=92=8C=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=AF=BB=E5=8F=96=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/__init__.py | 11 ++++-- httpfpt/cli.py | 4 +-- httpfpt/common/json_handler.py | 7 ++-- httpfpt/common/log.py | 4 +-- httpfpt/common/send_request.py | 22 ++++++------ httpfpt/common/toml_handler.py | 2 +- httpfpt/common/yaml_handler.py | 23 +++++------- httpfpt/conftest.py | 8 ++--- httpfpt/core/get_conf.py | 16 ++++----- httpfpt/core/path_conf.py | 20 ++++++----- httpfpt/db/mysql_db.py | 18 +++++----- httpfpt/db/redis_db.py | 12 +++---- httpfpt/run.py | 39 +++++++++++---------- httpfpt/utils/auth_plugins.py | 4 +-- httpfpt/utils/case_auto_generator.py | 10 +++--- httpfpt/utils/cli/about_testcase.py | 6 ++-- httpfpt/utils/data_manage/apifox.py | 16 +++++---- httpfpt/utils/data_manage/git_repo.py | 4 +-- httpfpt/utils/data_manage/openapi.py | 30 ++++++++-------- httpfpt/utils/file_control.py | 8 ++--- httpfpt/utils/request/case_data_parse.py | 2 +- httpfpt/utils/request/request_data_parse.py | 4 +-- httpfpt/utils/request/vars_extractor.py | 8 ++--- httpfpt/utils/send_report/ding_talk.py | 12 +++---- httpfpt/utils/send_report/lark_talk.py | 10 +++--- httpfpt/utils/send_report/send_email.py | 22 ++++++------ pdm.lock | 12 +++---- requirements.txt | 4 +-- 28 files changed, 171 insertions(+), 167 deletions(-) diff --git a/httpfpt/__init__.py b/httpfpt/__init__.py index bef887c..c2ba698 100644 --- a/httpfpt/__init__.py +++ b/httpfpt/__init__.py @@ -1,6 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from httpfpt.core.get_conf import set_project_config as set_project_config -from httpfpt.core.path_conf import set_project_dir as set_project_dir +from httpfpt.core.get_conf import set_httpfpt_config as set_httpfpt_config +from httpfpt.core.path_conf import set_httpfpt_dir as set_httpfpt_dir +from httpfpt.run import run as httpfpt_run __version__ = 'v0.5.1' + +__all__ = [ + 'set_httpfpt_config', + 'set_httpfpt_dir', + 'httpfpt_run', +] diff --git a/httpfpt/cli.py b/httpfpt/cli.py index ac8aa46..f7d3706 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -14,7 +14,7 @@ sys.path.append(os.path.dirname(os.path.dirname(__file__))) -from httpfpt import set_project_dir +from httpfpt import set_httpfpt_dir from httpfpt.utils.cli.about_testcase import generate_testcases, testcase_data_verify from httpfpt.utils.cli.import_case_data import ( import_apifox_case_data, @@ -68,7 +68,7 @@ def __call__(self) -> None: if self.version: get_version() if self.project_dir: - set_project_dir(self.project_dir) + set_httpfpt_dir(self.project_dir) if self.start_project: create_new_project(self.start_project) diff --git a/httpfpt/common/json_handler.py b/httpfpt/common/json_handler.py index 78db6ee..c36b9ef 100644 --- a/httpfpt/common/json_handler.py +++ b/httpfpt/common/json_handler.py @@ -8,10 +8,9 @@ from typing import Any from httpfpt.common.log import log -from httpfpt.core.path_conf import httpfpt_path_config -def read_json_file(filepath: str | None = httpfpt_path_config.case_data_dir, *, filename: str, **kwargs) -> dict: +def read_json_file(filepath: str, filename: str | None = None, **kwargs) -> dict: """ 读取 json 文件 @@ -20,10 +19,10 @@ def read_json_file(filepath: str | None = httpfpt_path_config.case_data_dir, *, :param kwargs: :return: """ - if filepath is not None: + if filename: filepath = os.path.join(filepath, filename) try: - with open(filepath, encoding='utf-8') as f: # type: ignore + with open(filepath, encoding='utf-8') as f: data = json.load(f, **kwargs) except Exception as e: log.error(f'文件 {filename} 读取错误: {e}') diff --git a/httpfpt/common/log.py b/httpfpt/common/log.py index 6449e89..c48ce1a 100644 --- a/httpfpt/common/log.py +++ b/httpfpt/common/log.py @@ -9,7 +9,7 @@ from loguru import logger -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.path_conf import httpfpt_path if TYPE_CHECKING: import loguru @@ -28,7 +28,7 @@ def log() -> loguru.Logger: :return: """ - log_path = httpfpt_path_config.log_dir + log_path = httpfpt_path.log_dir if not os.path.join(log_path): os.makedirs(log_path) diff --git a/httpfpt/common/send_request.py b/httpfpt/common/send_request.py index 1cd387c..07e872b 100644 --- a/httpfpt/common/send_request.py +++ b/httpfpt/common/send_request.py @@ -15,7 +15,7 @@ from httpfpt.common.errors import AssertError, SendRequestError from httpfpt.common.log import log -from httpfpt.core.get_conf import config +from httpfpt.core.get_conf import httpfpt_config from httpfpt.db.mysql_db import mysql_client from httpfpt.enums.request.body import BodyType from httpfpt.enums.request.engin import EnginType @@ -60,11 +60,11 @@ def _requests_engin(**kwargs) -> RequestsResponse: :param kwargs: :return: """ - kwargs['timeout'] = kwargs['timeout'] or config.REQUEST_TIMEOUT - kwargs['verify'] = kwargs['verify'] or config.REQUEST_VERIFY - kwargs['proxies'] = kwargs['proxies'] or config.REQUEST_PROXIES_REQUESTS - kwargs['allow_redirects'] = kwargs['allow_redirects'] or config.REQUEST_REDIRECTS - request_retry = kwargs['retry'] or config.REQUEST_RETRY + kwargs['timeout'] = kwargs['timeout'] or httpfpt_config.REQUEST_TIMEOUT + kwargs['verify'] = kwargs['verify'] or httpfpt_config.REQUEST_VERIFY + kwargs['proxies'] = kwargs['proxies'] or httpfpt_config.REQUEST_PROXIES_REQUESTS + kwargs['allow_redirects'] = kwargs['allow_redirects'] or httpfpt_config.REQUEST_REDIRECTS + request_retry = kwargs['retry'] or httpfpt_config.REQUEST_RETRY del kwargs['retry'] # 消除安全警告 requests.packages.urllib3.disable_warnings() # type: ignore @@ -90,11 +90,11 @@ def _httpx_engin(**kwargs) -> HttpxResponse: :param kwargs: :return: """ - kwargs['timeout'] = kwargs['timeout'] or config.REQUEST_TIMEOUT - verify = kwargs['verify'] or config.REQUEST_VERIFY - proxies = kwargs['proxies'] or config.REQUEST_PROXIES_HTTPX - redirects = kwargs['allow_redirects'] or config.REQUEST_REDIRECTS - request_retry = kwargs['retry'] or config.REQUEST_RETRY + kwargs['timeout'] = kwargs['timeout'] or httpfpt_config.REQUEST_TIMEOUT + verify = kwargs['verify'] or httpfpt_config.REQUEST_VERIFY + proxies = kwargs['proxies'] or httpfpt_config.REQUEST_PROXIES_HTTPX + redirects = kwargs['allow_redirects'] or httpfpt_config.REQUEST_REDIRECTS + request_retry = kwargs['retry'] or httpfpt_config.REQUEST_RETRY del kwargs['verify'] del kwargs['proxies'] del kwargs['allow_redirects'] diff --git a/httpfpt/common/toml_handler.py b/httpfpt/common/toml_handler.py index 7f12cce..6dd0055 100644 --- a/httpfpt/common/toml_handler.py +++ b/httpfpt/common/toml_handler.py @@ -9,7 +9,7 @@ from httpfpt.common.log import log -def read_toml(filepath: str, filename: str | None, encoding: str = 'utf-8') -> dict: +def read_toml(filepath: str, filename: str | None = None, encoding: str = 'utf-8') -> dict: """ 读取 toml 文件 diff --git a/httpfpt/common/yaml_handler.py b/httpfpt/common/yaml_handler.py index 8e2e6d6..c9d25db 100644 --- a/httpfpt/common/yaml_handler.py +++ b/httpfpt/common/yaml_handler.py @@ -10,11 +10,11 @@ import yaml from httpfpt.common.log import log -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.path_conf import httpfpt_path from httpfpt.utils.time_control import get_current_time -def read_yaml(filepath: str | None = httpfpt_path_config.case_data_dir, *, filename: str) -> dict[str, Any]: +def read_yaml(filepath: str, filename: str | None = None) -> dict[str, Any]: """ 读取 yaml 文件 @@ -22,15 +22,10 @@ def read_yaml(filepath: str | None = httpfpt_path_config.case_data_dir, *, filen :param filename: 文件名 :return: """ - if filepath is not None: - _file = os.path.join(filepath, filename) - else: - _file = filename - if not _file: - log.error('读取 yaml 文件失败,文件名为空') - raise FileNotFoundError('读取 yaml 文件失败,文件名为空') + if filename: + filepath = os.path.join(filepath, filename) try: - with open(_file, encoding='utf-8') as f: + with open(filepath, encoding='utf-8') as f: data = yaml.load(f, Loader=yaml.FullLoader) except Exception as e: log.error(f'文件 {filename} 读取错误: {e}') @@ -83,7 +78,7 @@ def write_yaml_report( :param mode: 文件写入模式 :return """ - _yaml_report_path = httpfpt_path_config.yaml_report_dir + _yaml_report_path = httpfpt_path.yaml_report_dir if not os.path.exists(_yaml_report_path): os.makedirs(_yaml_report_path) _file = os.path.join(_yaml_report_path, filename) @@ -104,9 +99,9 @@ def write_yaml_vars(data: dict) -> None: :param data: :return: """ - _file = os.path.join(httpfpt_path_config.data_path, 'global_vars.yaml') + _file = os.path.join(httpfpt_path.data_path, 'global_vars.yaml') try: - _vars = read_yaml(httpfpt_path_config.data_path, filename='global_vars.yaml') + _vars = read_yaml(httpfpt_path.data_path, filename='global_vars.yaml') _vars.update(data) with open(_file, encoding='utf-8', mode='w') as f: yaml.dump(_vars, f, allow_unicode=True) @@ -116,7 +111,7 @@ def write_yaml_vars(data: dict) -> None: log.info(f'写入全局变量成功: global_vars.yaml -> {data}') -def get_yaml_file(filepath: str = httpfpt_path_config.case_data_dir, *, filename: str) -> str: +def get_yaml_file(filepath: str = httpfpt_path.case_data_dir, *, filename: str) -> str: """ 获取 yaml 测试数据文件 diff --git a/httpfpt/conftest.py b/httpfpt/conftest.py index ba94a56..9ba658f 100644 --- a/httpfpt/conftest.py +++ b/httpfpt/conftest.py @@ -11,7 +11,7 @@ from httpfpt.common.log import log from httpfpt.common.variable_cache import variable_cache from httpfpt.common.yaml_handler import write_yaml_report -from httpfpt.core.get_conf import config as sys_config +from httpfpt.core.get_conf import httpfpt_config @pytest.fixture(scope='session', autouse=True) @@ -62,7 +62,7 @@ def pytest_configure(config): if metadata: from pytest_metadata.plugin import metadata_key - config.stash[metadata_key]['Project Name'] = sys_config.PROJECT_NAME + config.stash[metadata_key]['Project Name'] = httpfpt_config.PROJECT_NAME del config.stash[metadata_key]['Packages'] del config.stash[metadata_key]['Platform'] del config.stash[metadata_key]['Plugins'] @@ -76,7 +76,7 @@ def pytest_html_results_summary(prefix): :return: """ # 向 html 报告中的 summary 添加额外信息 - prefix.extend([html.p(f'Tester: {sys_config.TESTER_NAME}')]) + prefix.extend([html.p(f'Tester: {httpfpt_config.TESTER_NAME}')]) @pytest.mark.optionalhook @@ -87,7 +87,7 @@ def pytest_html_report_title(report): :param report: :return: """ - report.title = f'{sys_config.TEST_REPORT_TITLE}' + report.title = f'{httpfpt_config.TEST_REPORT_TITLE}' @pytest.mark.optionalhook diff --git a/httpfpt/core/get_conf.py b/httpfpt/core/get_conf.py index 16b3dbc..12e6986 100644 --- a/httpfpt/core/get_conf.py +++ b/httpfpt/core/get_conf.py @@ -7,12 +7,12 @@ from httpfpt.common.errors import ConfigInitError __all__ = [ - 'config', - 'set_project_config', + 'httpfpt_config', + 'set_httpfpt_config', ] # global config -config = None +httpfpt_config = None class HttpFptConfig: @@ -94,12 +94,12 @@ def __init__(self, settings: dict) -> None: self.REQUEST_RETRY = settings['request']['retry'] except KeyError as e: raise ConfigInitError( - f'配置文件解析失败:缺失参数 "{str(e)}"。请核对配置文件或字典。若尚未配置,可通过' + f'配置文件解析失败:缺失参数 "{str(e)}",请核对配置文件或字典,若尚未配置,可通过' f'set_project_config() 设置项目配置,或将 HTTPFPT_PROJECT_CONFIG 环境变量指向配置文件路径' ) -def set_project_config(settings: str | dict, config_filename: str | None = None) -> HttpFptConfig: +def set_httpfpt_config(settings: str | dict, config_filename: str | None = None) -> HttpFptConfig: """ 设置项目配置 @@ -107,7 +107,7 @@ def set_project_config(settings: str | dict, config_filename: str | None = None) :param config_filename: :return: """ - global config + global httpfpt_config if isinstance(settings, str): from httpfpt.common.toml_handler import read_toml @@ -118,5 +118,5 @@ def set_project_config(settings: str | dict, config_filename: str | None = None) else: conf = settings - config = HttpFptConfig(conf) - return config + httpfpt_config = HttpFptConfig(conf) + return httpfpt_config diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index d7ca86f..8acf34a 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -7,8 +7,8 @@ from httpfpt.common.errors import ConfigInitError __all__ = [ - 'httpfpt_path_config', - 'set_project_dir', + 'httpfpt_path', + 'set_httpfpt_dir', ] @@ -24,9 +24,9 @@ def __init__(self, base_dir: str) -> None: @property def project_dir(self) -> str: """项目根路径""" - if self.base_dir is None or not os.path.exists(self.base_dir): + if not os.path.exists(self.base_dir): self.base_dir = os.getenv('HTTPFPT_PROJECT_PATH') - if self.base_dir is None: + if not self.base_dir: raise ConfigInitError( '运行失败:在访问 httpfpt API 前,请先通过 set_project_dir() 方法设置项目根路径,' '或配置 HTTPFPT_PROJECT_PATH 环境变量' @@ -36,6 +36,8 @@ def project_dir(self) -> str: @property def log_dir(self) -> str: """日志路径""" + if not os.path.exists(self.base_dir): + return os.path.join(os.path.expanduser('~'), '.httpfpt') return os.path.join(self.project_dir, 'log') @property @@ -99,18 +101,18 @@ def auth_conf_dir(self) -> str: return os.path.join(self.project_dir, 'core') -def set_project_dir(base_dir: str) -> HttpFptPathConfig: +def set_httpfpt_dir(base_dir: str) -> HttpFptPathConfig: """ 设置项目目录 :param base_dir: 项目根路径 :return: """ - global httpfpt_path_config + global httpfpt_path - httpfpt_path_config = HttpFptPathConfig(base_dir) - return httpfpt_path_config + httpfpt_path = HttpFptPathConfig(base_dir) + return httpfpt_path # global path_config -httpfpt_path_config: HttpFptPathConfig = set_project_dir('') +httpfpt_path: HttpFptPathConfig = set_httpfpt_dir('') diff --git a/httpfpt/db/mysql_db.py b/httpfpt/db/mysql_db.py index 8a506ae..d0fdeef 100644 --- a/httpfpt/db/mysql_db.py +++ b/httpfpt/db/mysql_db.py @@ -17,8 +17,8 @@ from httpfpt.common.log import log from httpfpt.common.variable_cache import variable_cache from httpfpt.common.yaml_handler import write_yaml_vars -from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.get_conf import httpfpt_config +from httpfpt.core.path_conf import httpfpt_path from httpfpt.enums.query_fetch_type import QueryFetchType from httpfpt.enums.sql_type import SqlType from httpfpt.enums.var_type import VarType @@ -30,12 +30,12 @@ class MysqlDB: def __init__(self) -> None: self._pool = PooledDB( pymysql, - host=config.MYSQL_HOST, - port=config.MYSQL_PORT, - user=config.MYSQL_USER, - password=config.MYSQL_PASSWORD, - database=config.MYSQL_DATABASE, - charset=config.MYSQL_CHARSET, + host=httpfpt_config.MYSQL_HOST, + port=httpfpt_config.MYSQL_PORT, + user=httpfpt_config.MYSQL_USER, + password=httpfpt_config.MYSQL_PASSWORD, + database=httpfpt_config.MYSQL_DATABASE, + charset=httpfpt_config.MYSQL_CHARSET, maxconnections=15, blocking=True, # 连接池中如果没有可用连接后,是否阻塞等待 autocommit=False, # 是否自动提交 @@ -171,7 +171,7 @@ def exec_case_sql(self, sql: str, env_filename: str | None = None) -> dict | lis if set_type == VarType.CACHE: variable_cache.set(key, value_str) elif set_type == VarType.ENV: - write_env_vars(httpfpt_path_config.run_env_dir, env_filename, key, value_str) # type: ignore + write_env_vars(httpfpt_path.run_env_dir, env_filename, key, value_str) # type: ignore elif set_type == VarType.GLOBAL: write_yaml_vars({key: value_str}) else: diff --git a/httpfpt/db/redis_db.py b/httpfpt/db/redis_db.py index e71fe63..7ccb63d 100644 --- a/httpfpt/db/redis_db.py +++ b/httpfpt/db/redis_db.py @@ -7,17 +7,17 @@ from redis import AuthenticationError, Redis from httpfpt.common.log import log -from httpfpt.core.get_conf import config +from httpfpt.core.get_conf import httpfpt_config class RedisDB(Redis): def __init__(self) -> None: super().__init__( - host=config.REDIS_HOST, - port=config.REDIS_PORT, - password=config.REDIS_PASSWORD, - db=config.REDIS_DATABASE, - socket_timeout=config.REDIS_TIMEOUT, + host=httpfpt_config.REDIS_HOST, + port=httpfpt_config.REDIS_PORT, + password=httpfpt_config.REDIS_PASSWORD, + db=httpfpt_config.REDIS_DATABASE, + socket_timeout=httpfpt_config.REDIS_TIMEOUT, decode_responses=True, # 转码 utf-8 ) self.prefix = 'httpfpt' diff --git a/httpfpt/run.py b/httpfpt/run.py index 3b552f4..425b4da 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -14,8 +14,8 @@ from httpfpt.common.log import log from httpfpt.common.yaml_handler import read_yaml -from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.get_conf import httpfpt_config +from httpfpt.core.path_conf import httpfpt_path from httpfpt.db.redis_db import redis_client from httpfpt.utils.request import case_data_parse as case_data from httpfpt.utils.send_report.ding_talk import DingTalk @@ -42,7 +42,7 @@ def startup( """运行启动程序""" run_args = [log_level] - default_case_path = os.sep.join([os.path.dirname(__file__), 'testcases', config.PROJECT_NAME]) + default_case_path = os.sep.join([os.path.dirname(__file__), 'testcases', httpfpt_config.PROJECT_NAME]) if case_path: if '::' not in case_path: raise ValueError( @@ -61,19 +61,19 @@ def startup( run_path = default_case_path run_args.append(run_path) - html_report_filename = f'{config.PROJECT_NAME}_{get_current_time("%Y-%m-%d %H_%M_%S")}.html' + html_report_filename = f'{httpfpt_config.PROJECT_NAME}_{get_current_time("%Y-%m-%d %H_%M_%S")}.html' if html_report: - if not os.path.exists(httpfpt_path_config.html_report_dir): - os.makedirs(httpfpt_path_config.html_report_dir) + if not os.path.exists(httpfpt_path.html_report_dir): + os.makedirs(httpfpt_path.html_report_dir) run_args.extend( ( - f'--html={os.path.join(httpfpt_path_config.html_report_dir, html_report_filename)}', + f'--html={os.path.join(httpfpt_path.html_report_dir, html_report_filename)}', '--self-contained-html', ) ) if allure: - run_args.append(f'--alluredir={httpfpt_path_config.allure_report_dir}') + run_args.append(f'--alluredir={httpfpt_path.allure_report_dir}') if allure_clear: run_args.append('--clean-alluredir') @@ -113,34 +113,35 @@ def startup( format_run_args.append(i) run_pytest_command_args = ' '.join(_ for _ in format_run_args) - log.info(f'开始运行项目:{config.PROJECT_NAME}' if run_path == default_case_path else f'开始运行:{run_path}') + log.info( + f'开始运行项目:{httpfpt_config.PROJECT_NAME}' if run_path == default_case_path else f'开始运行:{run_path}' + ) log.info(f'Pytest CLI: pytest {run_pytest_command_args}') log.info('🚀 START') pytest.main(run_args) log.info('🏁 FINISH') - yaml_report_files = os.listdir(httpfpt_path_config.yaml_report_dir) + yaml_report_files = os.listdir(httpfpt_path.yaml_report_dir) yaml_report_files.sort() - test_result = read_yaml(httpfpt_path_config.yaml_report_dir, filename=yaml_report_files[-1]) + test_result = read_yaml(httpfpt_path.yaml_report_dir, filename=yaml_report_files[-1]) - if html_report and config.EMAIL_REPORT_SEND: + if html_report and httpfpt_config.EMAIL_REPORT_SEND: SendMail(test_result, html_report_filename).send_report() - if config.DING_TALK_REPORT_SEND: + if httpfpt_config.DING_TALK_REPORT_SEND: DingTalk(test_result).send() - if config.LARK_TALK_REPORT_SEND: + if httpfpt_config.LARK_TALK_REPORT_SEND: LarkTalk(test_result).send() if allure: - if not os.path.exists(httpfpt_path_config.allure_report_env_file): - shutil.copyfile(httpfpt_path_config.allure_env_file, httpfpt_path_config.allure_report_env_file) + if not os.path.exists(httpfpt_path.allure_report_env_file): + shutil.copyfile(httpfpt_path.allure_env_file, httpfpt_path.allure_report_env_file) if allure and allure_serve: os.popen( - f'allure generate {httpfpt_path_config.allure_report_dir} -o {httpfpt_path_config.allure_html_report_dir} ' - + '--clean' - ) and os.popen(f'allure serve {httpfpt_path_config.allure_report_dir}') # type: ignore + f'allure generate {httpfpt_path.allure_report_dir} -o {httpfpt_path.allure_html_report_dir} ' + '--clean' + ) and os.popen(f'allure serve {httpfpt_path.allure_report_dir}') # type: ignore def run( diff --git a/httpfpt/utils/auth_plugins.py b/httpfpt/utils/auth_plugins.py index 727ba33..f463dbe 100644 --- a/httpfpt/utils/auth_plugins.py +++ b/httpfpt/utils/auth_plugins.py @@ -10,7 +10,7 @@ from httpfpt.common.errors import AuthError, SendRequestError from httpfpt.common.yaml_handler import read_yaml -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.path_conf import httpfpt_path from httpfpt.db.redis_db import redis_client from httpfpt.enums.request.auth import AuthType from httpfpt.utils.enum_control import get_enum_values @@ -31,7 +31,7 @@ def __init__(self) -> None: @lru_cache def get_auth_data(self) -> dict: - auth_data = read_yaml(httpfpt_path_config.auth_conf_dir, filename='auth.yaml') + auth_data = read_yaml(httpfpt_path.auth_conf_dir, filename='auth.yaml') return auth_data def request_auth(self) -> requests.Response: diff --git a/httpfpt/utils/case_auto_generator.py b/httpfpt/utils/case_auto_generator.py index b364337..f74d2bd 100644 --- a/httpfpt/utils/case_auto_generator.py +++ b/httpfpt/utils/case_auto_generator.py @@ -4,8 +4,8 @@ from pathlib import Path -from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.get_conf import httpfpt_config +from httpfpt.core.path_conf import httpfpt_path from httpfpt.utils.file_control import get_file_property, search_all_case_data_files, search_all_testcase_files from httpfpt.utils.rich_console import console @@ -88,15 +88,15 @@ def {testcase_func_name}(self, data): send_request.send_request(data) ''' # 创建测试用例文件 - tag = case_filename.split(config.PROJECT_NAME)[1].split(os.path.sep)[1:-1] + tag = case_filename.split(httpfpt_config.PROJECT_NAME)[1].split(os.path.sep)[1:-1] new_testcase_filename = testcase_func_name + '.py' if tag: case_path = os.path.join( - httpfpt_path_config.testcase_dir, config.PROJECT_NAME, *tag, new_testcase_filename + httpfpt_path.testcase_dir, httpfpt_config.PROJECT_NAME, *tag, new_testcase_filename ) else: case_path = os.path.join( - httpfpt_path_config.testcase_dir, config.PROJECT_NAME, new_testcase_filename + httpfpt_path.testcase_dir, httpfpt_config.PROJECT_NAME, new_testcase_filename ) new_testcase_dir = Path(case_path).parent if not new_testcase_dir.exists(): diff --git a/httpfpt/utils/cli/about_testcase.py b/httpfpt/utils/cli/about_testcase.py index bea71a7..63d9ce0 100644 --- a/httpfpt/utils/cli/about_testcase.py +++ b/httpfpt/utils/cli/about_testcase.py @@ -25,13 +25,13 @@ def testcase_data_verify(verify: str) -> None: for file in file_list: file_type = get_file_property(file)[2] if file_type == CaseDataType.JSON: - file_data = read_json_file(None, filename=file) + file_data = read_json_file(file) else: - file_data = read_yaml(None, filename=file) + file_data = read_yaml(file) CaseData.model_validate(file_data) else: console.print(f'🔥 开始验证 {verify} 测试数据结构...') - file_data = read_yaml(None, filename=verify) + file_data = read_yaml(verify) CaseData.model_validate(file_data) except ValidationError as e: count = e.error_count() diff --git a/httpfpt/utils/data_manage/apifox.py b/httpfpt/utils/data_manage/apifox.py index 60d6332..71a504a 100644 --- a/httpfpt/utils/data_manage/apifox.py +++ b/httpfpt/utils/data_manage/apifox.py @@ -7,8 +7,8 @@ from httpfpt.common.json_handler import read_json_file from httpfpt.common.yaml_handler import write_yaml -from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.get_conf import httpfpt_config +from httpfpt.core.path_conf import httpfpt_path from httpfpt.utils.data_manage.base_format import format_value from httpfpt.utils.file_control import get_file_property from httpfpt.utils.rich_console import console @@ -23,7 +23,7 @@ def import_apifox_to_yaml(self, source: str, project: str | None = None) -> None :param project: :return: """ - data = read_json_file(None, filename=source) + data = read_json_file(source) apifox = data.get('apifoxProject') if not apifox: raise ValueError('获取 apifox 数据失败,请使用合法的 apifox 文件') @@ -63,8 +63,10 @@ def import_apifox_to_yaml(self, source: str, project: str | None = None) -> None case_config['allure']['feature'] = case_config['module'] = k case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - httpfpt_path_config.case_data_dir, - os.sep.join([project or config.PROJECT_NAME, k, get_file_property(source)[1] + '.yaml']), + httpfpt_path.case_data_dir, + os.sep.join( + [project or httpfpt_config.PROJECT_NAME, k, get_file_property(source)[1] + '.yaml'] + ), case_file_data, mode='w', ) @@ -73,8 +75,8 @@ def import_apifox_to_yaml(self, source: str, project: str | None = None) -> None for v in root_case.values(): case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - httpfpt_path_config.case_data_dir, - os.sep.join([project or config.PROJECT_NAME, get_file_property(source)[1] + '.yaml']), + httpfpt_path.case_data_dir, + os.sep.join([project or httpfpt_config.PROJECT_NAME, get_file_property(source)[1] + '.yaml']), case_file_data, mode='w', ) diff --git a/httpfpt/utils/data_manage/git_repo.py b/httpfpt/utils/data_manage/git_repo.py index c591400..f9fc926 100644 --- a/httpfpt/utils/data_manage/git_repo.py +++ b/httpfpt/utils/data_manage/git_repo.py @@ -50,9 +50,9 @@ def import_git_to_local(src: str) -> None: for file in all_case_data_file: file_type = get_file_property(file)[2] if file_type == CaseDataType.JSON: - file_data = read_json_file(None, filename=file) + file_data = read_json_file(file) else: - file_data = read_yaml(None, filename=file) + file_data = read_yaml(file) all_case_data.append(file_data) count: int = 0 for case_data in all_case_data: diff --git a/httpfpt/utils/data_manage/openapi.py b/httpfpt/utils/data_manage/openapi.py index 83130ca..1303635 100644 --- a/httpfpt/utils/data_manage/openapi.py +++ b/httpfpt/utils/data_manage/openapi.py @@ -12,8 +12,8 @@ from httpfpt.common.json_handler import read_json_file from httpfpt.common.yaml_handler import write_yaml -from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.get_conf import httpfpt_config +from httpfpt.core.path_conf import httpfpt_path from httpfpt.utils.data_manage.base_format import format_value from httpfpt.utils.file_control import get_file_property from httpfpt.utils.rich_console import console @@ -138,7 +138,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None for k, v in tag_case.items(): tag_filename = os.sep.join( [ - project or config.PROJECT_NAME, + project or httpfpt_config.PROJECT_NAME, k, get_file_property(openapi_source)[1] + '.yaml' if not openapi_source.startswith('http') @@ -151,7 +151,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None if k == 'root': root_filename = os.sep.join( [ - project or config.PROJECT_NAME, + project or httpfpt_config.PROJECT_NAME, get_file_property(openapi_source)[1] + '.yaml' if not openapi_source.startswith('http') else f'openapi_{get_current_timestamp()}.yaml', @@ -160,7 +160,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None else: root_filename = os.sep.join( [ - project or config.PROJECT_NAME, + project or httpfpt_config.PROJECT_NAME, get_file_property(openapi_source)[1] + '.yaml' if not openapi_source.startswith('http') else f'openapi_{k}.yaml', @@ -183,10 +183,10 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None case_config['allure']['feature'] = case_config['module'] = k case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - httpfpt_path_config.case_data_dir, + httpfpt_path.case_data_dir, os.sep.join( [ - project or config.PROJECT_NAME, + project or httpfpt_config.PROJECT_NAME, k, get_file_property(openapi_source)[1] + '.yaml' if not openapi_source.startswith('http') @@ -213,8 +213,8 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None ) case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - httpfpt_path_config.case_data_dir, - os.sep.join([project or config.PROJECT_NAME, filename]), + httpfpt_path.case_data_dir, + os.sep.join([project or httpfpt_config.PROJECT_NAME, filename]), case_file_data, mode='w', ) @@ -230,7 +230,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None case_file_data = {'config': case_config, 'test_steps': v} tag_filename = os.sep.join( [ - project or config.PROJECT_NAME, + project or httpfpt_config.PROJECT_NAME, k, get_file_property(openapi_source)[1] + '.yaml' if not openapi_source.startswith('http') @@ -239,9 +239,7 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None ) is_write = Confirm.ask(f'❓ 是否需要创建 {tag_filename} 数据文件?', default=True) if is_write: - write_yaml( - httpfpt_path_config.case_data_dir, tag_filename, case_file_data, mode='w' - ) + write_yaml(httpfpt_path.case_data_dir, tag_filename, case_file_data, mode='w') # 写入项目根目录 if len(root_case) > 0: for k, v in root_case.items(): @@ -261,8 +259,8 @@ def import_openapi_to_yaml(self, openapi_source: str, project: str | None = None if is_write: case_file_data = {'config': case_config, 'test_steps': v} write_yaml( - httpfpt_path_config.case_data_dir, - os.sep.join([project or config.PROJECT_NAME, root_filename]), + httpfpt_path.case_data_dir, + os.sep.join([project or httpfpt_config.PROJECT_NAME, root_filename]), case_file_data, mode='w', ) @@ -286,7 +284,7 @@ def get_swagger_data(self, openapi_source: str) -> None: if not (openapi or swagger): raise ValueError('请输入正确的 openapi 地址') else: - data = read_json_file(None, filename=openapi_source) + data = read_json_file(openapi_source) openapi = data.get('openapi') swagger = data.get('swagger') if not (openapi or swagger): diff --git a/httpfpt/utils/file_control.py b/httpfpt/utils/file_control.py index be7f3eb..bcc3946 100644 --- a/httpfpt/utils/file_control.py +++ b/httpfpt/utils/file_control.py @@ -7,8 +7,8 @@ from pathlib import Path -from httpfpt.core.get_conf import config -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.get_conf import httpfpt_config +from httpfpt.core.path_conf import httpfpt_path def get_file_property(filepath: str) -> tuple[str, str, str]: @@ -32,7 +32,7 @@ def search_all_case_data_files(filepath: str | None = None) -> list: :return: """ case_data_filepath = ( - os.path.join(httpfpt_path_config.case_data_dir, f'{config.PROJECT_NAME}') if filepath is None else filepath + os.path.join(httpfpt_path.case_data_dir, f'{httpfpt_config.PROJECT_NAME}') if filepath is None else filepath ) files = ( glob.glob(os.path.join(case_data_filepath, '**', '*.yaml'), recursive=True) @@ -49,7 +49,7 @@ def search_all_testcase_files() -> list: :return: """ files = glob.glob( - os.path.join(httpfpt_path_config.testcase_dir, f'{config.PROJECT_NAME}', '**', 'test_*.py'), recursive=True + os.path.join(httpfpt_path.testcase_dir, f'{httpfpt_config.PROJECT_NAME}', '**', 'test_*.py'), recursive=True ) return files diff --git a/httpfpt/utils/request/case_data_parse.py b/httpfpt/utils/request/case_data_parse.py index b8a1005..b5d06c8 100644 --- a/httpfpt/utils/request/case_data_parse.py +++ b/httpfpt/utils/request/case_data_parse.py @@ -40,7 +40,7 @@ def case_data_init(pydantic_verify: bool) -> None: """ all_case_data_files = search_all_case_data_files() for case_data_file in all_case_data_files: - case_data = read_yaml(None, filename=case_data_file) + case_data = read_yaml(case_data_file) filename = get_file_property(case_data_file)[0] file_hash = get_file_hash(case_data_file) case_data.update({'filename': filename, 'file_hash': file_hash}) diff --git a/httpfpt/utils/request/request_data_parse.py b/httpfpt/utils/request/request_data_parse.py index 9f71a7b..7b74ff5 100644 --- a/httpfpt/utils/request/request_data_parse.py +++ b/httpfpt/utils/request/request_data_parse.py @@ -15,7 +15,7 @@ from httpfpt.common.env_handler import get_env_dict from httpfpt.common.errors import RequestDataParseError from httpfpt.common.log import log -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.path_conf import httpfpt_path from httpfpt.db.mysql_db import mysql_client from httpfpt.enums.allure_severity_type import SeverityType from httpfpt.enums.request.auth import AuthType @@ -331,7 +331,7 @@ def url(self) -> str: if not url.startswith('http'): _env = self.env try: - env_file = os.path.join(httpfpt_path_config.run_env_dir, _env) + env_file = os.path.join(httpfpt_path.run_env_dir, _env) env_dict = get_env_dict(env_file) except Exception as e: raise RequestDataParseError(f'环境变量 {_env} 读取失败: {e}') diff --git a/httpfpt/utils/request/vars_extractor.py b/httpfpt/utils/request/vars_extractor.py index 0f5b1ff..4080176 100644 --- a/httpfpt/utils/request/vars_extractor.py +++ b/httpfpt/utils/request/vars_extractor.py @@ -13,7 +13,7 @@ from httpfpt.common.log import log from httpfpt.common.variable_cache import variable_cache from httpfpt.common.yaml_handler import read_yaml, write_yaml_vars -from httpfpt.core.path_conf import httpfpt_path_config +from httpfpt.core.path_conf import httpfpt_path from httpfpt.enums.var_type import VarType @@ -48,13 +48,13 @@ def vars_replace(self, target: dict, env_filename: str | None = None) -> dict: if not env or not isinstance(env, str): raise RequestDataParseError('运行环境获取失败, 测试用例数据缺少 config:request:env 参数') try: - env_file = os.path.join(httpfpt_path_config.run_env_dir, env) + env_file = os.path.join(httpfpt_path.run_env_dir, env) env_vars = get_env_dict(env_file) except OSError: raise RequestDataParseError('运行环境获取失败, 请检查测试用例环境配置') # 获取全局变量 - global_vars = read_yaml(httpfpt_path_config.data_path, filename='global_vars.yaml') + global_vars = read_yaml(httpfpt_path.data_path, filename='global_vars.yaml') # 获取 re 规则字符串 var_re = self.sql_vars_re if env_filename else self.vars_re @@ -149,7 +149,7 @@ def teardown_var_extract(response: dict, extract: dict, env: str) -> None: if set_type == VarType.CACHE: variable_cache.set(key, value_str) elif set_type == VarType.ENV: - write_env_vars(httpfpt_path_config.run_env_dir, env, key, value_str) + write_env_vars(httpfpt_path.run_env_dir, env, key, value_str) elif set_type == VarType.GLOBAL: write_yaml_vars({key: value_str}) else: diff --git a/httpfpt/utils/send_report/ding_talk.py b/httpfpt/utils/send_report/ding_talk.py index 271c707..c4b7b6b 100644 --- a/httpfpt/utils/send_report/ding_talk.py +++ b/httpfpt/utils/send_report/ding_talk.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from httpfpt.common.log import log -from httpfpt.core.get_conf import config +from httpfpt.core.get_conf import httpfpt_config class DingTalk: @@ -16,9 +16,9 @@ def send(self) -> None: data = { 'msgtype': 'markdown', 'markdown': { - 'title': config.TEST_REPORT_TITLE, - 'text': f"> ## {config.PROJECT_NAME} 自动化测试报告\n\n" - f"> 👤 测试人员: {config.TESTER_NAME}\n\n" + 'title': httpfpt_config.TEST_REPORT_TITLE, + 'text': f"> ## {httpfpt_config.PROJECT_NAME} 自动化测试报告\n\n" + f"> 👤 测试人员: {httpfpt_config.TESTER_NAME}\n\n" f"> 🤖 测试结果: {self.content['result']}\n\n" f"> ✅ 通过用例: {self.content['passed']}\n\n" f"> 🔧 失败用例: {self.content['failed']}\n\n" @@ -30,10 +30,10 @@ def send(self) -> None: }, } response = requests.session().post( - url=config.DING_TALK_WEBHOOK, + url=httpfpt_config.DING_TALK_WEBHOOK, json=data, headers=headers, - proxies=config.DING_TALK_PROXY, # type: ignore + proxies=httpfpt_config.DING_TALK_PROXY, # type: ignore ) response.raise_for_status() except Exception as e: diff --git a/httpfpt/utils/send_report/lark_talk.py b/httpfpt/utils/send_report/lark_talk.py index 3bfe5e9..10279eb 100644 --- a/httpfpt/utils/send_report/lark_talk.py +++ b/httpfpt/utils/send_report/lark_talk.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from httpfpt.common.log import log -from httpfpt.core.get_conf import config +from httpfpt.core.get_conf import httpfpt_config class LarkTalk: @@ -19,9 +19,9 @@ def send(self) -> None: 'content': { 'post': { 'zh_cn': { - 'title': config.TEST_REPORT_TITLE, + 'title': httpfpt_config.TEST_REPORT_TITLE, 'content': [ - [{'tag': 'text', 'text': f'👤 测试人员: {config.TESTER_NAME}'}], + [{'tag': 'text', 'text': f'👤 测试人员: {httpfpt_config.TESTER_NAME}'}], [{'tag': 'text', 'text': f"🤖 测试结果: {self.content['result']}"}], [{'tag': 'text', 'text': f"✅ 通过用例: {self.content['passed']}"}], [{'tag': 'text', 'text': f"🔧 失败用例: {self.content['failed']}"}], @@ -36,10 +36,10 @@ def send(self) -> None: }, } response = requests.session().post( - url=config.LARK_TALK_WEBHOOK, + url=httpfpt_config.LARK_TALK_WEBHOOK, json=data, headers=headers, - proxies=config.LARK_TALK_PROXY, # type: ignore + proxies=httpfpt_config.LARK_TALK_PROXY, # type: ignore ) response.raise_for_status() except Exception as e: diff --git a/httpfpt/utils/send_report/send_email.py b/httpfpt/utils/send_report/send_email.py index 55628e3..d90fc84 100644 --- a/httpfpt/utils/send_report/send_email.py +++ b/httpfpt/utils/send_report/send_email.py @@ -12,7 +12,7 @@ from jinja2 import Template from httpfpt.common.log import log -from httpfpt.core.get_conf import config +from httpfpt.core.get_conf import httpfpt_config from httpfpt.enums.email_type import EmailType from httpfpt.utils.file_control import get_file_property @@ -27,11 +27,11 @@ def take_report(self) -> MIMEMultipart: 获取报告 """ msg = MIMEMultipart() - msg['Subject'] = config.TEST_REPORT_TITLE - msg['From'] = config.EMAIL_USER + msg['Subject'] = httpfpt_config.TEST_REPORT_TITLE + msg['From'] = httpfpt_config.EMAIL_USER msg['date'] = time.strftime('%a, %d %b %Y %H:%M:%S %z') - self.content.update({'test_title': config.TEST_REPORT_TITLE}) - self.content.update({'tester_name': config.TESTER_NAME}) + self.content.update({'test_title': httpfpt_config.TEST_REPORT_TITLE}) + self.content.update({'tester_name': httpfpt_config.TESTER_NAME}) # 邮件正文 with open('../../templates/email_notification.html', 'r', encoding='utf-8') as f: @@ -58,7 +58,7 @@ def take_error(self) -> MIMEMultipart: """ msg = MIMEMultipart() msg['Subject'] = 'HttpFpt 运行异常通知' - msg['From'] = config.EMAIL_USER + msg['From'] = httpfpt_config.EMAIL_USER msg['date'] = time.strftime('%a, %d %b %Y %H:%M:%S %z') # 邮件正文 @@ -79,12 +79,12 @@ def _send(self, msg_type: int) -> None: msg = self.take_report().as_string() elif msg_type == EmailType.ERROR: msg = self.take_error().as_string() - if config.EMAIL_SSL: - smtp = smtplib.SMTP_SSL(host=config.EMAIL_SERVER, port=config.EMAIL_PORT) + if httpfpt_config.EMAIL_SSL: + smtp = smtplib.SMTP_SSL(host=httpfpt_config.EMAIL_SERVER, port=httpfpt_config.EMAIL_PORT) else: - smtp = smtplib.SMTP(host=config.EMAIL_SERVER, port=config.EMAIL_PORT) - smtp.login(config.EMAIL_USER, config.EMAIL_PASSWORD) - smtp.sendmail(config.EMAIL_USER, config.EMAIL_SEND_TO, msg) + smtp = smtplib.SMTP(host=httpfpt_config.EMAIL_SERVER, port=httpfpt_config.EMAIL_PORT) + smtp.login(httpfpt_config.EMAIL_USER, httpfpt_config.EMAIL_PASSWORD) + smtp.sendmail(httpfpt_config.EMAIL_USER, httpfpt_config.EMAIL_SEND_TO, msg) smtp.quit() def send_report(self) -> None: diff --git a/pdm.lock b/pdm.lock index 7965132..8d4cf34 100644 --- a/pdm.lock +++ b/pdm.lock @@ -93,7 +93,7 @@ files = [ [[package]] name = "cappa" -version = "0.16.6" +version = "0.17.0" requires_python = ">=3.8,<4" summary = "Declarative CLI argument parser." dependencies = [ @@ -103,8 +103,8 @@ dependencies = [ "typing-inspect>=0.9.0", ] files = [ - {file = "cappa-0.16.6-py3-none-any.whl", hash = "sha256:d3195edba6109e49597a46fc632ffca036f0ffcc8ed9c157e983fd0581c73df1"}, - {file = "cappa-0.16.6.tar.gz", hash = "sha256:14a534d58b909624623ae72547e0c09a1ef4e76bbc14592c3bb4ecc6ef394e65"}, + {file = "cappa-0.17.0-py3-none-any.whl", hash = "sha256:3e620a9e947c784bc327d58cce9f6f8f824b51782ef3cfaccb06cef9d1f458a7"}, + {file = "cappa-0.17.0.tar.gz", hash = "sha256:9a3c45255e2da19c397343dff0164dd0bbdaec9a1ca4721b692421b0c7b6cdd4"}, ] [[package]] @@ -372,7 +372,7 @@ files = [ [[package]] name = "faker" -version = "23.3.0" +version = "24.0.0" requires_python = ">=3.8" summary = "Faker is a Python package that generates fake data for you." dependencies = [ @@ -380,8 +380,8 @@ dependencies = [ "typing-extensions>=3.10.0.1; python_version <= \"3.8\"", ] files = [ - {file = "Faker-23.3.0-py3-none-any.whl", hash = "sha256:117ce1a2805c1bc5ca753b3dc6f9d567732893b2294b827d3164261ee8f20267"}, - {file = "Faker-23.3.0.tar.gz", hash = "sha256:458d93580de34403a8dec1e8d5e6be2fee96c4deca63b95d71df7a6a80a690de"}, + {file = "Faker-24.0.0-py3-none-any.whl", hash = "sha256:2456d674f40bd51eb3acbf85221277027822e529a90cc826453d9a25dff932b1"}, + {file = "Faker-24.0.0.tar.gz", hash = "sha256:ea6f784c40730de0f77067e49e78cdd590efb00bec3d33f577492262206c17fc"}, ] [[package]] diff --git a/requirements.txt b/requirements.txt index 3b5613e..78fdd60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ anyio==3.7.1 async-timeout==4.0.3; python_full_version <= "3.11.2" attrs==23.2.0 cache3==0.4.3 -cappa==0.16.6 +cappa==0.17.0 certifi==2023.11.17 cffi==1.16.0 cfgv==3.4.0 @@ -20,7 +20,7 @@ dirty-equals==0.7.1 distlib==0.3.8 eval-type-backport==0.1.3 exceptiongroup==1.2.0; python_version < "3.11" -faker==23.3.0 +faker==24.0.0 filelock==3.12.4 h11==0.12.0 hiredis==2.3.2 From 830e7376df8c96e4fd0fbe5b80a35e5cd374fe65 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Wed, 6 Mar 2024 11:10:30 +0800 Subject: [PATCH 17/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=94=9F=E6=88=90cli?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/utils/cli/new_project.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index 09b61a4..c1c4dae 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -26,15 +26,15 @@ def create_new_project(start_project: tuple[str, str]) -> None: run_settings_path = os.path.join(project_path, 'core', 'conf.toml') init_tpl = f"""#!/usr/bin/env python3 # -*- coding: utf-8 -*- -from httpfpt import set_project_dir -from httpfpt import set_project_config +from httpfpt import set_httpfpt_dir +from httpfpt import set_httpfpt_config -set_project_dir({project_path}) -set_project_config({run_settings_path}) +set_httpfpt_dir({project_path}) +set_httpfpt_config({run_settings_path}) """ run_tpl = """#!/usr/bin/env python3 # -*- coding: utf-8 -*- -from httpfpt.run import run as httpfpt_run +from httpfpt import httpfpt_run httpfpt_run() From c61526d82885188d35eff9fc1bfc8a86a93b0d13 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Wed, 6 Mar 2024 11:42:21 +0800 Subject: [PATCH 18/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/core/path_conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index 8acf34a..e1b85ec 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -24,7 +24,7 @@ def __init__(self, base_dir: str) -> None: @property def project_dir(self) -> str: """项目根路径""" - if not os.path.exists(self.base_dir): + if not self.base_dir or not os.path.exists(self.base_dir): self.base_dir = os.getenv('HTTPFPT_PROJECT_PATH') if not self.base_dir: raise ConfigInitError( @@ -36,7 +36,7 @@ def project_dir(self) -> str: @property def log_dir(self) -> str: """日志路径""" - if not os.path.exists(self.base_dir): + if not self.base_dir or not os.path.exists(self.base_dir): return os.path.join(os.path.expanduser('~'), '.httpfpt') return os.path.join(self.project_dir, 'log') From d725cd9033921a38d1df330a1db59dd1b7a81691 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Wed, 6 Mar 2024 15:39:45 +0800 Subject: [PATCH 19/46] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=E9=AA=8C=E8=AF=81cli=E5=8F=AA=E4=BD=BF=E7=94=A8=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/__init__.py | 7 ------- httpfpt/cli.py | 23 +++++------------------ httpfpt/common/yaml_handler.py | 14 -------------- httpfpt/utils/cli/about_testcase.py | 15 ++++++++++++++- 4 files changed, 19 insertions(+), 40 deletions(-) diff --git a/httpfpt/__init__.py b/httpfpt/__init__.py index c2ba698..457c84d 100644 --- a/httpfpt/__init__.py +++ b/httpfpt/__init__.py @@ -2,12 +2,5 @@ # -*- coding: utf-8 -*- from httpfpt.core.get_conf import set_httpfpt_config as set_httpfpt_config from httpfpt.core.path_conf import set_httpfpt_dir as set_httpfpt_dir -from httpfpt.run import run as httpfpt_run __version__ = 'v0.5.1' - -__all__ = [ - 'set_httpfpt_config', - 'set_httpfpt_dir', - 'httpfpt_run', -] diff --git a/httpfpt/cli.py b/httpfpt/cli.py index f7d3706..70b95fb 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -14,7 +14,6 @@ sys.path.append(os.path.dirname(os.path.dirname(__file__))) -from httpfpt import set_httpfpt_dir from httpfpt.utils.cli.about_testcase import generate_testcases, testcase_data_verify from httpfpt.utils.cli.import_case_data import ( import_apifox_case_data, @@ -24,7 +23,6 @@ import_openapi_case_data, import_postman_case_data, ) -from httpfpt.utils.cli.new_project import create_new_project from httpfpt.utils.cli.version import get_version @@ -40,17 +38,6 @@ class HttpFptCLI: help='Print version information.', ), ] - project_dir: Annotated[ - str, - cappa.Arg( - value_name='', - short=True, - long=True, - default='.', - help='Set the project directory path.', - required=False, - ), - ] start_project: Annotated[ tuple[str, str], cappa.Arg( @@ -67,10 +54,10 @@ class HttpFptCLI: def __call__(self) -> None: if self.version: get_version() - if self.project_dir: - set_httpfpt_dir(self.project_dir) if self.start_project: - create_new_project(self.start_project) + pass + # https://github.com/DanCardin/cappa/issues/106 + # create_new_project(self.start_project) @cappa.command(name='testcase', help='Test case tools.') @@ -83,7 +70,7 @@ class TestCaseCLI: short='-dv', long=True, default='', - help='验证测试数据结构;当指定文件时, 仅验证指定文件, 当指定 "all" 时, 验证所有文件.', + help='验证测试数据结构;当指定文件(文件名/完整路径)时, 仅验证指定文件, 当指定 "all" 时, 验证所有文件.', required=False, ), ] @@ -105,7 +92,7 @@ def __call__(self) -> None: generate_testcases() -@cappa.command(name='import', help='Import test case data.') +@cappa.command(name='import', help='Import testcase data.') @dataclass class ImportCLI: openai: Annotated[ diff --git a/httpfpt/common/yaml_handler.py b/httpfpt/common/yaml_handler.py index c9d25db..38c4b18 100644 --- a/httpfpt/common/yaml_handler.py +++ b/httpfpt/common/yaml_handler.py @@ -109,17 +109,3 @@ def write_yaml_vars(data: dict) -> None: log.error(f'写入 global_vars.yaml 全局变量 {data} 错误: {e}') else: log.info(f'写入全局变量成功: global_vars.yaml -> {data}') - - -def get_yaml_file(filepath: str = httpfpt_path.case_data_dir, *, filename: str) -> str: - """ - 获取 yaml 测试数据文件 - - :param filepath: 文件路径 - :param filename: 文件名 - :return: - """ - _file = os.path.join(filepath, filename) - if not os.path.exists(_file): - raise FileNotFoundError(f'测试数据文件 {filename} 不存在') - return _file diff --git a/httpfpt/utils/cli/about_testcase.py b/httpfpt/utils/cli/about_testcase.py index 63d9ce0..9df745c 100644 --- a/httpfpt/utils/cli/about_testcase.py +++ b/httpfpt/utils/cli/about_testcase.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import os.path + import cappa from pydantic import ValidationError @@ -7,6 +9,7 @@ from httpfpt.common.json_handler import read_json_file from httpfpt.common.yaml_handler import read_yaml +from httpfpt.core.path_conf import httpfpt_path from httpfpt.enums.case_data_type import CaseDataType from httpfpt.schemas.case_data import CaseData from httpfpt.utils.case_auto_generator import auto_generate_testcases @@ -31,7 +34,17 @@ def testcase_data_verify(verify: str) -> None: CaseData.model_validate(file_data) else: console.print(f'🔥 开始验证 {verify} 测试数据结构...') - file_data = read_yaml(verify) + file_type = get_file_property(verify)[2] + if os.path.isfile(verify): + if file_type == CaseDataType.JSON: + file_data = read_json_file(httpfpt_path.case_data_dir, verify) + else: + file_data = read_yaml(httpfpt_path.case_data_dir, verify) + else: + if file_type == CaseDataType.JSON: + file_data = read_json_file(verify) + else: + file_data = read_yaml(verify) CaseData.model_validate(file_data) except ValidationError as e: count = e.error_count() From f4a89dc5b3366bff90bb146445c45f6b8377a8dc Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 7 Mar 2024 15:31:34 +0800 Subject: [PATCH 20/46] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 7 +++-- httpfpt/run.py | 9 ++++++ httpfpt/testcases/__init__.py | 2 -- httpfpt/testcases/test_project/__init__.py | 2 -- httpfpt/testcases/test_project/test_api.py | 30 ------------------- .../test_api_testcase_template.py | 24 --------------- .../testcases/test_project/test_only_skip.py | 24 --------------- .../test_project/test_upload_file.py | 24 --------------- 8 files changed, 13 insertions(+), 109 deletions(-) delete mode 100644 httpfpt/testcases/__init__.py delete mode 100644 httpfpt/testcases/test_project/__init__.py delete mode 100644 httpfpt/testcases/test_project/test_api.py delete mode 100644 httpfpt/testcases/test_project/test_api_testcase_template.py delete mode 100644 httpfpt/testcases/test_project/test_only_skip.py delete mode 100644 httpfpt/testcases/test_project/test_upload_file.py diff --git a/httpfpt/cli.py b/httpfpt/cli.py index 70b95fb..8804fc8 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -23,6 +23,7 @@ import_openapi_case_data, import_postman_case_data, ) +from httpfpt.utils.cli.new_project import create_new_project from httpfpt.utils.cli.version import get_version @@ -55,9 +56,9 @@ def __call__(self) -> None: if self.version: get_version() if self.start_project: - pass - # https://github.com/DanCardin/cappa/issues/106 - # create_new_project(self.start_project) + # pass + # # https://github.com/DanCardin/cappa/issues/106 + create_new_project(self.start_project) @cappa.command(name='testcase', help='Test case tools.') diff --git a/httpfpt/run.py b/httpfpt/run.py index 425b4da..01b0ec2 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -17,6 +17,7 @@ from httpfpt.core.get_conf import httpfpt_config from httpfpt.core.path_conf import httpfpt_path from httpfpt.db.redis_db import redis_client +from httpfpt.utils.case_auto_generator import auto_generate_testcases from httpfpt.utils.request import case_data_parse as case_data from httpfpt.utils.send_report.ding_talk import DingTalk from httpfpt.utils.send_report.lark_talk import LarkTalk @@ -26,6 +27,7 @@ def startup( *args, + testcase_generate: bool, log_level: Literal['-q', '-s', '-v', '-vv'], case_path: str | None, html_report: bool, @@ -40,6 +42,9 @@ def startup( **kwargs, ) -> None: """运行启动程序""" + if testcase_generate: + auto_generate_testcases(testcase_generate) + run_args = [log_level] default_case_path = os.sep.join([os.path.dirname(__file__), 'testcases', httpfpt_config.PROJECT_NAME]) @@ -146,6 +151,8 @@ def startup( def run( *args, + # auto testcases + testcase_generate: bool = False, # init clean_cache: bool = False, pydantic_verify: bool = True, @@ -171,6 +178,7 @@ def run( 运行入口 :param args: pytest 运行参数 + :param testcase_generate: 自动生成测试用例(危险行为,自动强制覆盖同名文件),建议通过 CLI 手动执行,默认关闭 :param clean_cache: 清理 redis 缓存数据,对于脏数据,这很有用,默认关闭 :param pydantic_verify: 用例数据完整架构 pydantic 快速检测, 默认开启 :param args: pytest 运行参数 @@ -207,6 +215,7 @@ def run( case_data.case_id_unique_verify() startup( *args, + testcase_generate=testcase_generate, log_level=log_level, case_path=case_path, html_report=html_report, diff --git a/httpfpt/testcases/__init__.py b/httpfpt/testcases/__init__.py deleted file mode 100644 index 56fafa5..0000000 --- a/httpfpt/testcases/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- diff --git a/httpfpt/testcases/test_project/__init__.py b/httpfpt/testcases/test_project/__init__.py deleted file mode 100644 index 56fafa5..0000000 --- a/httpfpt/testcases/test_project/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- diff --git a/httpfpt/testcases/test_project/test_api.py b/httpfpt/testcases/test_project/test_api.py deleted file mode 100644 index 269f219..0000000 --- a/httpfpt/testcases/test_project/test_api.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import allure -import pytest - -from dirty_equals import IsInt - -from httpfpt.common.log import log - - -@allure.epic('demo') -@allure.feature('示例') -class TestDemo: - """Demo""" - - @allure.story('简单输出') - @pytest.mark.test_mark - @pytest.mark.parametrize('a, b', [(1, 2)]) - def test_001(self, a, b): - """测试001""" - log.info("This is a demo's test") - assert a != b - - @allure.story('xfall输出') - @pytest.mark.xfail - @pytest.mark.test_mark - def test_002(self): - """测试002""" - log.info('这是一个框架xfail修饰测试') - assert 1 == IsInt diff --git a/httpfpt/testcases/test_project/test_api_testcase_template.py b/httpfpt/testcases/test_project/test_api_testcase_template.py deleted file mode 100644 index b3715f2..0000000 --- a/httpfpt/testcases/test_project/test_api_testcase_template.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import allure -import pytest - -from httpfpt.common.send_request import send_request -from httpfpt.utils.request.case_data_parse import get_request_data -from httpfpt.utils.request.ids_extract import get_ids - -request_data = get_request_data(filename='api_testcase_template.yaml') -allure_text = request_data[0]['config']['allure'] -request_ids = get_ids(request_data) - - -@allure.epic(allure_text['epic']) -@allure.feature(allure_text['feature']) -class TestApiTestcaseTemplate: - """ApicaseTemplate""" - - @allure.story(allure_text['story']) - @pytest.mark.parametrize('data', request_data, ids=request_ids) - def test_api_testcase_template(self, data): - """api_testcase_template""" - send_request.send_request(data) diff --git a/httpfpt/testcases/test_project/test_only_skip.py b/httpfpt/testcases/test_project/test_only_skip.py deleted file mode 100644 index 518d3b4..0000000 --- a/httpfpt/testcases/test_project/test_only_skip.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import allure -import pytest - -from httpfpt.common.send_request import send_request -from httpfpt.utils.request.case_data_parse import get_request_data -from httpfpt.utils.request.ids_extract import get_ids - -request_data = get_request_data(filename='only_skip.yml') -allure_text = request_data[0]['config']['allure'] -request_ids = get_ids(request_data) - - -@allure.epic(allure_text['epic']) -@allure.feature(allure_text['feature']) -class TestOnlySkip: - """OnlySkip""" - - @allure.story(allure_text['story']) - @pytest.mark.parametrize('data', request_data, ids=request_ids) - def test_only_skip(self, data): - """only_skip""" - send_request.send_request(data) diff --git a/httpfpt/testcases/test_project/test_upload_file.py b/httpfpt/testcases/test_project/test_upload_file.py deleted file mode 100644 index f8168de..0000000 --- a/httpfpt/testcases/test_project/test_upload_file.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import allure -import pytest - -from httpfpt.common.send_request import send_request -from httpfpt.utils.request.case_data_parse import get_request_data -from httpfpt.utils.request.ids_extract import get_ids - -request_data = get_request_data(filename='upload_file.json') -allure_text = request_data[0]['config']['allure'] -request_ids = get_ids(request_data) - - -@allure.epic(allure_text['epic']) -@allure.feature(allure_text['feature']) -class TestUploadFile: - """UploadFile""" - - @allure.story(allure_text['story']) - @pytest.mark.parametrize('data', request_data, ids=request_ids) - def test_upload_file(self, data): - """upload_file""" - send_request.send_request(data) From a2f67c3d2bb0b0a9760159315c44ccef3bc45993 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 7 Mar 2024 15:48:42 +0800 Subject: [PATCH 21/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7=E4=B8=BA0.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpfpt/__init__.py b/httpfpt/__init__.py index 457c84d..7f91614 100644 --- a/httpfpt/__init__.py +++ b/httpfpt/__init__.py @@ -3,4 +3,4 @@ from httpfpt.core.get_conf import set_httpfpt_config as set_httpfpt_config from httpfpt.core.path_conf import set_httpfpt_dir as set_httpfpt_dir -__version__ = 'v0.5.1' +__version__ = 'v0.6.0' From 41851d339473f7aa716f76a5f805dbe9a302f5a4 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 7 Mar 2024 16:33:27 +0800 Subject: [PATCH 22/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=9B=B8=E5=AF=B9?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 2 +- httpfpt/utils/cli/version.py | 4 +++- httpfpt/utils/send_report/send_email.py | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index 8804fc8..ea7caaa 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -45,7 +45,7 @@ class HttpFptCLI: value_name='', short=False, long='--startproject', - default=('httpfpt_project', '.'), + default=(), help='Create a new project.', required=False, ), diff --git a/httpfpt/utils/cli/version.py b/httpfpt/utils/cli/version.py index 7d176e6..e5ee61f 100644 --- a/httpfpt/utils/cli/version.py +++ b/httpfpt/utils/cli/version.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- import re +from importlib.resources import read_text + import cappa from httpfpt.utils.rich_console import console @@ -9,7 +11,7 @@ def get_version() -> None: """获取版本号""" - ver = open('./__init__.py', 'rt').read() + ver = read_text('httpfpt', '__init__.py') mob = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", ver, re.MULTILINE) if mob: console.print('\n🔥 HttpFpt', mob.group(1)) diff --git a/httpfpt/utils/send_report/send_email.py b/httpfpt/utils/send_report/send_email.py index d90fc84..da35f0d 100644 --- a/httpfpt/utils/send_report/send_email.py +++ b/httpfpt/utils/send_report/send_email.py @@ -8,6 +8,7 @@ from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +from importlib.resources import open_text from jinja2 import Template @@ -34,7 +35,7 @@ def take_report(self) -> MIMEMultipart: self.content.update({'tester_name': httpfpt_config.TESTER_NAME}) # 邮件正文 - with open('../../templates/email_notification.html', 'r', encoding='utf-8') as f: + with open_text('httpfpt.templates', 'email_notification.html') as f: html = Template(f.read()) mail_body = MIMEText(html.render(**self.content), _subtype='html', _charset='utf-8') @@ -62,7 +63,7 @@ def take_error(self) -> MIMEMultipart: msg['date'] = time.strftime('%a, %d %b %Y %H:%M:%S %z') # 邮件正文 - with open('../../templates/email_notification.html', 'r', encoding='utf-8') as f: + with open_text('httpfpt.templates', 'email_notification.html') as f: html = Template(f.read()) mail_body = MIMEText(html.render(**self.content), _subtype='html', _charset='utf-8') From b0e2457c0a303f68255e3f09fa842308d09a6ed9 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 7 Mar 2024 18:40:01 +0800 Subject: [PATCH 23/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 2 -- httpfpt/utils/cli/new_project.py | 49 +++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index ea7caaa..19c2770 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -56,8 +56,6 @@ def __call__(self) -> None: if self.version: get_version() if self.start_project: - # pass - # # https://github.com/DanCardin/cappa/issues/106 create_new_project(self.start_project) diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index c1c4dae..3267c7e 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -5,36 +5,62 @@ import os import shutil +from importlib.resources import path as import_path + import cappa +from httpfpt.utils.rich_console import console + def create_new_project(start_project: tuple[str, str]) -> None: name = start_project[0] path = start_project[1] if path != '.': if not os.path.isdir(path): - raise cappa.Exit(f'"{path}" is not a directory', code=1) - project_path = os.path.join(path, name) + raise cappa.Exit(f'\n"{path}" is not a directory', code=1) + project_path = os.path.abspath(os.sep.join([path, name])) + core_path = os.path.join(project_path, 'core') + data_path = os.path.join(project_path, 'data') + conftest_file = os.path.join(project_path, 'conftest.py') + pytest_file = os.path.join(project_path, 'pytest.ini') if os.path.exists(project_path): - raise cappa.Exit('The project directory is not empty', code=1) + raise cappa.Exit(f'\nThe "{name}" directory is not empty', code=1) os.makedirs(project_path) - shutil.copytree('./core', project_path, ignore=shutil.ignore_patterns('get_conf.py', 'path_conf.py')) - shutil.copytree('./data', project_path) - shutil.copytree('./testcases', project_path) - shutil.copyfile('conftest.py', project_path) - shutil.copyfile('pytest.ini', project_path) + with import_path('httpfpt.core', '') as core_data: + shutil.copytree(core_data, core_path, ignore=shutil.ignore_patterns('get_conf.py', 'path_conf.py')) + with import_path('httpfpt.data', '') as case_data: + shutil.copytree(case_data, data_path) + with import_path('httpfpt', 'conftest.py') as conftest: + shutil.copyfile(conftest, conftest_file) + with import_path('httpfpt', 'pytest.ini') as pytest_ini: + shutil.copyfile(pytest_ini, pytest_file) run_settings_path = os.path.join(project_path, 'core', 'conf.toml') init_tpl = f"""#!/usr/bin/env python3 # -*- coding: utf-8 -*- +from functools import wraps +from typing import Any, Callable + from httpfpt import set_httpfpt_dir from httpfpt import set_httpfpt_config -set_httpfpt_dir({project_path}) -set_httpfpt_config({run_settings_path}) + +# Init setup +set_httpfpt_dir('{project_path}') +set_httpfpt_config('{run_settings_path}') + + +def ensure_httpfpt_setup(func: Callable) -> Any: + @wraps(func) + def wrapper(*args, **kwargs) -> Callable: + set_httpfpt_dir('{project_path}') + set_httpfpt_config('{run_settings_path}') + return func(*args, **kwargs) + + return wrapper """ run_tpl = """#!/usr/bin/env python3 # -*- coding: utf-8 -*- -from httpfpt import httpfpt_run +from httpfpt.run import run as httpfpt_run httpfpt_run() @@ -43,3 +69,4 @@ def create_new_project(start_project: tuple[str, str]) -> None: f.write(init_tpl) with open(os.path.join(project_path, 'run.py'), 'w', encoding='utf-8') as f: f.write(run_tpl) + console.print(f'\n🎉 The project "{name}" has been created') From 78030084e8c3c79365fa724975d37323a1151277 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 8 Mar 2024 00:55:28 +0800 Subject: [PATCH 24/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=91=BD=E4=BB=A4=E4=B8=BA=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 8 +++++--- httpfpt/utils/cli/new_project.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index 19c2770..0003533 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -10,6 +10,7 @@ import cappa from cappa import Subcommands +from rich.traceback import install as rich_install from typing_extensions import Annotated sys.path.append(os.path.dirname(os.path.dirname(__file__))) @@ -40,12 +41,12 @@ class HttpFptCLI: ), ] start_project: Annotated[ - tuple[str, str], + bool, cappa.Arg( value_name='', short=False, long='--startproject', - default=(), + default=False, help='Create a new project.', required=False, ), @@ -56,7 +57,7 @@ def __call__(self) -> None: if self.version: get_version() if self.start_project: - create_new_project(self.start_project) + create_new_project() @cappa.command(name='testcase', help='Test case tools.') @@ -175,6 +176,7 @@ def __call__(self) -> None: def cappa_invoke() -> None: """cli 执行程序""" + rich_install() cappa.invoke(HttpFptCLI) diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index 3267c7e..68dc990 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -9,15 +9,17 @@ import cappa +from rich.prompt import Prompt + from httpfpt.utils.rich_console import console -def create_new_project(start_project: tuple[str, str]) -> None: - name = start_project[0] - path = start_project[1] +def create_new_project() -> None: + name = Prompt.ask('Set your project a name', default='httpfpt_project') + path = Prompt.ask('Set your project path (relative or absolute path, which automatically parses.)', default='.') if path != '.': if not os.path.isdir(path): - raise cappa.Exit(f'\n"{path}" is not a directory', code=1) + raise cappa.Exit(f'\nThe "{path}" is not a directory', code=1) project_path = os.path.abspath(os.sep.join([path, name])) core_path = os.path.join(project_path, 'core') data_path = os.path.join(project_path, 'data') @@ -69,4 +71,7 @@ def wrapper(*args, **kwargs) -> Callable: f.write(init_tpl) with open(os.path.join(project_path, 'run.py'), 'w', encoding='utf-8') as f: f.write(run_tpl) - console.print(f'\n🎉 The project "{name}" has been created') + console.print( + f'\n🎉 The project "{name}" has been created.' + f'\n🌴 The project is located in the directory: [cyan]{project_path}[/]' + ) From 8144e7ca32fc3b3dc4a08452ad89ab41bbb7e4b1 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 8 Mar 2024 01:07:41 +0800 Subject: [PATCH 25/46] =?UTF-8?q?=E5=9B=BA=E5=AE=9Acappa=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E4=B8=BA0.17.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 16 +++++++--------- httpfpt/utils/cli/about_testcase.py | 11 +++++------ httpfpt/utils/cli/import_case_data.py | 6 +++--- httpfpt/utils/cli/new_project.py | 8 ++++---- httpfpt/utils/cli/version.py | 4 ++-- pdm.lock | 8 ++++---- pyproject.toml | 2 +- requirements.txt | 2 +- 8 files changed, 27 insertions(+), 30 deletions(-) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index 0003533..1524e81 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -44,7 +44,6 @@ class HttpFptCLI: bool, cappa.Arg( value_name='', - short=False, long='--startproject', default=False, help='Create a new project.', @@ -67,7 +66,7 @@ class TestCaseCLI: str, cappa.Arg( value_name='', - short='-dv', + short='-c', long=True, default='', help='验证测试数据结构;当指定文件(文件名/完整路径)时, 仅验证指定文件, 当指定 "all" 时, 验证所有文件.', @@ -77,7 +76,7 @@ class TestCaseCLI: generate: Annotated[ bool, cappa.Arg( - short='-gt', + short='-g', long=True, default=False, help='自动生成测试用例.', @@ -99,7 +98,7 @@ class ImportCLI: tuple[str, str], cappa.Arg( value_name=' ', - short='-io', + short='-o', long='--import-openapi', default=(), help='导入 openapi 数据到 yaml 数据文件; 支持 json 文件 / url 导入, 需要指定 project 项目名.', @@ -110,7 +109,7 @@ class ImportCLI: tuple[str, str], cappa.Arg( value_name=' ', - short='-ia', + short='-a', long='--import-apifox', default=(), help='Beta: 导入 apifox 数据到 yaml 数据文件; 支持 json 文件导入, 需要指定 project 项目名.', @@ -120,7 +119,7 @@ class ImportCLI: har: Annotated[ tuple[str, str], cappa.Arg( - short='-ih', + short='-h', long='--import-har', default=(), help='TODO: Not started yet.', @@ -130,7 +129,7 @@ class ImportCLI: jmeter: Annotated[ tuple[str, str], cappa.Arg( - short='-ij', + short='-j', long='--import-jmeter', default=(), help='TODO: Not started yet.', @@ -140,7 +139,7 @@ class ImportCLI: postman: Annotated[ tuple[str, str], cappa.Arg( - short='-ip', + short='-p', long='--import-postman', default=(), help='TODO: Not started yet.', @@ -151,7 +150,6 @@ class ImportCLI: str, cappa.Arg( value_name='', - short='-ig', long='--import-git', default='', help='导入 git 仓库测试数据到本地.', diff --git a/httpfpt/utils/cli/about_testcase.py b/httpfpt/utils/cli/about_testcase.py index 9df745c..1f36ac1 100644 --- a/httpfpt/utils/cli/about_testcase.py +++ b/httpfpt/utils/cli/about_testcase.py @@ -50,11 +50,10 @@ def testcase_data_verify(verify: str) -> None: count = e.error_count() msg += str(e) except Exception as e: - console.print(f'❌ 验证测试数据 {verify} 结构失败: {e}') - raise cappa.Exit(code=1) + console.print(f'\n❌ 验证测试数据 {verify} 结构失败: {e}') + raise e if count > 0: - console.print(f'❌ 验证测试数据 {verify} 结构失败: {msg}') - raise cappa.Exit(code=1) + raise cappa.Exit(f'\n❌ 验证测试数据 {verify} 结构失败: {msg}', code=1) else: console.print('✅ 验证测试数据结构成功') @@ -78,5 +77,5 @@ def generate_testcases() -> None: console.print('🔥 开始生成新测试用例...') auto_generate_testcases() except Exception as e: - console.print(f'❌ 自动生成测试用例失败: {e}') - raise cappa.Exit(code=1) + console.print(f'\n❌ 自动生成测试用例失败: {e}') + raise e diff --git a/httpfpt/utils/cli/import_case_data.py b/httpfpt/utils/cli/import_case_data.py index 64bb85b..3bdabe3 100644 --- a/httpfpt/utils/cli/import_case_data.py +++ b/httpfpt/utils/cli/import_case_data.py @@ -19,7 +19,7 @@ def import_openapi_case_data(openapi: tuple[str, str]) -> None: try: SwaggerParser().import_openapi_to_yaml(openapi[0], openapi[1]) except Exception as e: - console.print('❌ 导入 openapi 数据失败') + console.print('\n❌ 导入 openapi 数据失败') raise e @@ -37,7 +37,7 @@ def import_apifox_case_data(apifox: tuple[str, str]) -> None: try: ApiFoxParser().import_apifox_to_yaml(apifox[0], apifox[1]) except Exception as e: - console.print('❌ 导入 apifox 数据失败:') + console.print('\n❌ 导入 apifox 数据失败:') raise e @@ -63,5 +63,5 @@ def import_git_case_data(src: str) -> None: try: GitRepoPaser.import_git_to_local(src) except Exception as e: - console.print(f'❌ 导入 git 仓库测试数据失败: {e}') + console.print(f'\n❌ 导入 git 仓库测试数据失败: {e}') raise e diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index 68dc990..3f72e2b 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -15,18 +15,18 @@ def create_new_project() -> None: - name = Prompt.ask('Set your project a name', default='httpfpt_project') - path = Prompt.ask('Set your project path (relative or absolute path, which automatically parses.)', default='.') + name = Prompt.ask('❓ Set your project a name', default='httpfpt_project') + path = Prompt.ask('❓ Set your project path (relative or absolute path, which automatically parses.)', default='.') if path != '.': if not os.path.isdir(path): - raise cappa.Exit(f'\nThe "{path}" is not a directory', code=1) + raise cappa.Exit(f'\n❌ The "{path}" is not a directory', code=1) project_path = os.path.abspath(os.sep.join([path, name])) core_path = os.path.join(project_path, 'core') data_path = os.path.join(project_path, 'data') conftest_file = os.path.join(project_path, 'conftest.py') pytest_file = os.path.join(project_path, 'pytest.ini') if os.path.exists(project_path): - raise cappa.Exit(f'\nThe "{name}" directory is not empty', code=1) + raise cappa.Exit(f'\n❌ The "{name}" directory is not empty', code=1) os.makedirs(project_path) with import_path('httpfpt.core', '') as core_data: shutil.copytree(core_data, core_path, ignore=shutil.ignore_patterns('get_conf.py', 'path_conf.py')) diff --git a/httpfpt/utils/cli/version.py b/httpfpt/utils/cli/version.py index e5ee61f..f5197d0 100644 --- a/httpfpt/utils/cli/version.py +++ b/httpfpt/utils/cli/version.py @@ -14,6 +14,6 @@ def get_version() -> None: ver = read_text('httpfpt', '__init__.py') mob = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", ver, re.MULTILINE) if mob: - console.print('\n🔥 HttpFpt', mob.group(1)) + console.print(f'\n🔥 HttpFpt [cyan]{mob.group(1)}[/]') else: - raise cappa.Exit('未查询到版本号', code=1) + raise cappa.Exit('\n❌ 未查询到版本号', code=1) diff --git a/pdm.lock b/pdm.lock index 8d4cf34..9037bf6 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "test"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:305d3f78fbc861987db46e3f1a7387d4987017fb727d5afca28933af64d996dc" +content_hash = "sha256:08a740389fea6f46200dc53b108ab00c998d58f827040d253fdda2de6c57fb1f" [[package]] name = "allure-pytest" @@ -93,7 +93,7 @@ files = [ [[package]] name = "cappa" -version = "0.17.0" +version = "0.17.1" requires_python = ">=3.8,<4" summary = "Declarative CLI argument parser." dependencies = [ @@ -103,8 +103,8 @@ dependencies = [ "typing-inspect>=0.9.0", ] files = [ - {file = "cappa-0.17.0-py3-none-any.whl", hash = "sha256:3e620a9e947c784bc327d58cce9f6f8f824b51782ef3cfaccb06cef9d1f458a7"}, - {file = "cappa-0.17.0.tar.gz", hash = "sha256:9a3c45255e2da19c397343dff0164dd0bbdaec9a1ca4721b692421b0c7b6cdd4"}, + {file = "cappa-0.17.1-py3-none-any.whl", hash = "sha256:ad811902315333cff88ab15e98e0818a3702c474783138a7c7148c420077a773"}, + {file = "cappa-0.17.1.tar.gz", hash = "sha256:94156a65a7e7db9f85f11fa197784664ccd0338970a59d444f7f8c16891eb7cc"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 54fe859..35c9ff8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ dependencies = [ "allure-pytest==2.13.2", "cache3>=0.4.3", - "cappa>=0.16.3", + "cappa==0.17.1", "cryptography==41.0.6", "dbutils==3.0.2", "dirty-equals==0.7.1", diff --git a/requirements.txt b/requirements.txt index 78fdd60..1e012b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ anyio==3.7.1 async-timeout==4.0.3; python_full_version <= "3.11.2" attrs==23.2.0 cache3==0.4.3 -cappa==0.17.0 +cappa==0.17.1 certifi==2023.11.17 cffi==1.16.0 cfgv==3.4.0 From 344470faf12ed2c76b5c6aefcd79c0fb5521a42a Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Fri, 8 Mar 2024 19:02:54 +0800 Subject: [PATCH 26/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=92=8C=E9=85=8D=E7=BD=AE=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + httpfpt/__init__.py | 1 - httpfpt/core/conf.toml | 16 +- httpfpt/core/get_conf.py | 201 ++++++++++-------- httpfpt/core/path_conf.py | 53 ++--- httpfpt/run.py | 24 ++- httpfpt/utils/cli/new_project.py | 8 +- .../send_report/{ding_talk.py => ding.py} | 6 +- .../send_report/{send_email.py => email.py} | 2 +- .../send_report/{lark_talk.py => feishu.py} | 6 +- pdm.lock | 37 +++- pyproject.toml | 11 + requirements.txt | 3 + 13 files changed, 222 insertions(+), 147 deletions(-) rename httpfpt/utils/send_report/{ding_talk.py => ding.py} (91%) rename httpfpt/utils/send_report/{send_email.py => email.py} (99%) rename httpfpt/utils/send_report/{lark_talk.py => feishu.py} (93%) diff --git a/.gitignore b/.gitignore index bf9f2c7..3437bcf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ httpfpt/log/ .idea/ httpfpt/data/online* .ruff_cache/ +testcases/ diff --git a/httpfpt/__init__.py b/httpfpt/__init__.py index 7f91614..34386f8 100644 --- a/httpfpt/__init__.py +++ b/httpfpt/__init__.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from httpfpt.core.get_conf import set_httpfpt_config as set_httpfpt_config from httpfpt.core.path_conf import set_httpfpt_dir as set_httpfpt_dir __version__ = 'v0.6.0' diff --git a/httpfpt/core/conf.toml b/httpfpt/core/conf.toml index a8d8830..081b471 100644 --- a/httpfpt/core/conf.toml +++ b/httpfpt/core/conf.toml @@ -1,6 +1,6 @@ # 测试项目 [project] -project = 'test_project' +name = 'test_project' # 测试报告 [report] @@ -26,27 +26,27 @@ timeout = 10 # 邮件 [email] -host_server = 'smtp.qq.com' +host = 'smtp.qq.com' port = 465 user = '12345678@qq.com' password = '12345678' -send_to = '87654321@qq.com' # if more, use: [str] +receiver = '87654321@qq.com' # if more, use: [str] ssl = true -send_report = false +send = false # 钉钉 -[ding_talk] +[ding] webhook = '' proxies.http = '' proxies.https = '' -send_report = false +send = false # 飞书 -[lark_talk] +[lark] webhook = '' proxies.http = '' proxies.https = '' -send_report = false +send = false # 请求发送 diff --git a/httpfpt/core/get_conf.py b/httpfpt/core/get_conf.py index 12e6986..27d9ecb 100644 --- a/httpfpt/core/get_conf.py +++ b/httpfpt/core/get_conf.py @@ -4,119 +4,144 @@ import os.path -from httpfpt.common.errors import ConfigInitError +from glom import glom -__all__ = [ - 'httpfpt_config', - 'set_httpfpt_config', -] +from httpfpt.common.errors import ConfigInitError +from httpfpt.core.path_conf import httpfpt_path -# global config -httpfpt_config = None +__all__ = ['httpfpt_config'] class HttpFptConfig: - def __init__(self, settings: dict) -> None: + def __init__(self) -> None: + # 项目目录名 + self.PROJECT_NAME = None + # 测试报告 + self.TEST_REPORT_TITLE = None + self.TESTER_NAME = None + # mysql 数据库 + self.MYSQL_HOST = None + self.MYSQL_PORT = None + self.MYSQL_USER = None + self.MYSQL_PASSWORD = None + self.MYSQL_DATABASE = None + self.MYSQL_CHARSET = None + # redis 数据库 + self.REDIS_HOST = None + self.REDIS_PORT = None + self.REDIS_PASSWORD = None + self.REDIS_DATABASE = None + self.REDIS_TIMEOUT = None + # 邮件 + self.EMAIL_SERVER = None + self.EMAIL_PORT = None + self.EMAIL_USER = None + self.EMAIL_PASSWORD = None + self.EMAIL_SEND_TO = None + self.EMAIL_SSL = None + self.EMAIL_SEND = None + # 钉钉 + self.DINGDING_WEBHOOK = None + self.DINGDING_PROXY = None + self.DINGDING_SEND = None + # 飞书 + self.FEISHU_WEBHOOK = None + self.FEISHU_PROXY = None + self.FEISHU_SEND = None + # 请求发送 + self.REQUEST_TIMEOUT = None + self.REQUEST_VERIFY = None + self.REQUEST_REDIRECTS = None + self.REQUEST_PROXIES_REQUESTS = None + self.REQUEST_PROXIES_HTTPX = None + self.REQUEST_RETRY = None + + def __call__(self, settings: str | dict, config_filename: str | None = None) -> HttpFptConfig: """ - 项目配置初始化 + 设置项目配置 - :param settings: + :param settings: 项目配置,字典或指定 toml 配置文件 + :param config_filename: + :return: """ + if isinstance(settings, str): + from httpfpt.common.toml_handler import read_toml + + if not os.path.isdir(settings) and not os.path.isfile(settings): + raise ConfigInitError('配置获取失败,请检查配置文件路径是否合法') + self.settings = read_toml(settings, config_filename) + else: + self.settings = settings try: - # 项目目录名 - self.PROJECT_NAME = settings['project']['project'] - # 测试报告 - self.TEST_REPORT_TITLE = settings['report']['title'] - self.TESTER_NAME = settings['report']['tester_name'] - # mysql 数据库 - self.MYSQL_HOST = settings['mysql']['host'] - self.MYSQL_PORT = settings['mysql']['port'] - self.MYSQL_USER = settings['mysql']['user'] - self.MYSQL_PASSWORD = settings['mysql']['password'] - self.MYSQL_DATABASE = settings['mysql']['database'] - self.MYSQL_CHARSET = settings['mysql']['charset'] - # redis 数据库 - self.REDIS_HOST = settings['redis']['host'] - self.REDIS_PORT = settings['redis']['port'] - self.REDIS_PASSWORD = settings['redis']['password'] - self.REDIS_DATABASE = settings['redis']['database'] - self.REDIS_TIMEOUT = settings['redis']['timeout'] - # 邮件 - self.EMAIL_SERVER = settings['email']['host_server'] - self.EMAIL_PORT = settings['email']['port'] - self.EMAIL_USER = settings['email']['user'] - self.EMAIL_PASSWORD = settings['email']['password'] - self.EMAIL_SEND_TO = settings['email']['send_to'] - self.EMAIL_SSL = settings['email']['ssl'] - self.EMAIL_REPORT_SEND = settings['email']['send_report'] - # 钉钉 - self.DING_TALK_WEBHOOK = settings['ding_talk']['webhook'] - self.DING_TALK_PROXY = { - 'http': settings['ding_talk']['proxies']['http'] - if settings['ding_talk']['proxies']['http'] != '' + self.PROJECT_NAME = glom(self.settings, 'project.name') + self.TEST_REPORT_TITLE = glom(self.settings, 'report.title') + self.TESTER_NAME = glom(self.settings, 'report.tester_name') + self.MYSQL_HOST = glom(self.settings, 'mysql.host') + self.MYSQL_PORT = glom(self.settings, 'mysql.port') + self.MYSQL_USER = glom(self.settings, 'mysql.user') + self.MYSQL_PASSWORD = glom(self.settings, 'mysql.password') + self.MYSQL_DATABASE = glom(self.settings, 'mysql.database') + self.MYSQL_CHARSET = glom(self.settings, 'mysql.charset') + self.REDIS_HOST = glom(self.settings, 'redis.host') + self.REDIS_PORT = glom(self.settings, 'redis.port') + self.REDIS_PASSWORD = glom(self.settings, 'redis.password') + self.REDIS_DATABASE = glom(self.settings, 'redis.database') + self.REDIS_TIMEOUT = glom(self.settings, 'redis.timeout') + self.EMAIL_SERVER = glom(self.settings, 'email.host') + self.EMAIL_PORT = glom(self.settings, 'email.port') + self.EMAIL_USER = glom(self.settings, 'email.user') + self.EMAIL_PASSWORD = glom(self.settings, 'email.password') + self.EMAIL_SEND_TO = glom(self.settings, 'email.receiver') + self.EMAIL_SSL = glom(self.settings, 'email.ssl') + self.EMAIL_SEND = glom(self.settings, 'email.send') + self.DINGDING_WEBHOOK = glom(self.settings, 'ding.webhook') + self.DINGDING_PROXY = { + 'http': glom(self.settings, 'ding.proxies.http') + if glom(self.settings, 'ding.proxies.http') != '' else None, - 'https': settings['ding_talk']['proxies']['https'] - if settings['ding_talk']['proxies']['https'] != '' + 'https': glom(self.settings, 'ding.proxies.https') + if glom(self.settings, 'ding.proxies.https') != '' else None, } - self.DING_TALK_REPORT_SEND = settings['ding_talk']['send_report'] - # 飞书 - self.LARK_TALK_WEBHOOK = settings['lark_talk']['webhook'] - self.LARK_TALK_PROXY = { - 'http': settings['lark_talk']['proxies']['http'] - if settings['lark_talk']['proxies']['http'] != '' + self.DINGDING_SEND = glom(self.settings, 'ding.send') + self.FEISHU_WEBHOOK = glom(self.settings, 'lark.webhook') + self.FEISHU_PROXY = { + 'http': glom(self.settings, 'lark.proxies.http') + if glom(self.settings, 'lark.proxies.http') != '' else None, - 'https': settings['lark_talk']['proxies']['https'] - if settings['lark_talk']['proxies']['https'] != '' + 'https': glom(self.settings, 'lark.proxies.https') + if glom(self.settings, 'lark.proxies.https') != '' else None, } - self.LARK_TALK_REPORT_SEND = settings['lark_talk']['send_report'] - # 请求发送 - self.REQUEST_TIMEOUT = settings['request']['timeout'] - self.REQUEST_VERIFY = settings['request']['verify'] - self.REQUEST_REDIRECTS = settings['request']['redirects'] + self.FEISHU_SEND = glom(self.settings, 'lark.send') + self.REQUEST_TIMEOUT = glom(self.settings, 'request.timeout') + self.REQUEST_VERIFY = glom(self.settings, 'request.verify') + self.REQUEST_REDIRECTS = glom(self.settings, 'request.redirects') self.REQUEST_PROXIES_REQUESTS = { - 'http': settings['request']['proxies']['http'] - if settings['request']['proxies']['http'] != '' + 'http': glom(self.settings, 'request.proxies.http') + if glom(self.settings, 'request.proxies.http') != '' else None, - 'https': settings['request']['proxies']['https'] - if settings['request']['proxies']['https'] != '' + 'https': glom(self.settings, 'request.proxies.https') + if glom(self.settings, 'request.proxies.https') != '' else None, } self.REQUEST_PROXIES_HTTPX = { - 'http://': settings['request']['proxies']['http'] - if settings['request']['proxies']['http'] != '' + 'http://': glom(self.settings, 'request.proxies.http') + if glom(self.settings, 'request.proxies.http') != '' else None, - 'https://': settings['request']['proxies']['https'] - if settings['request']['proxies']['https'] != '' + 'https://': glom(self.settings, 'request.proxies.https') + if glom(self.settings, 'request.proxies.https') != '' else None, } - self.REQUEST_RETRY = settings['request']['retry'] + self.REQUEST_RETRY = glom(self.settings, 'request.retry') except KeyError as e: - raise ConfigInitError( - f'配置文件解析失败:缺失参数 "{str(e)}",请核对配置文件或字典,若尚未配置,可通过' - f'set_project_config() 设置项目配置,或将 HTTPFPT_PROJECT_CONFIG 环境变量指向配置文件路径' - ) - + raise ConfigInitError(f'配置解析失败:缺失参数 {str(e)},请核对配置文件或字典') -def set_httpfpt_config(settings: str | dict, config_filename: str | None = None) -> HttpFptConfig: - """ - 设置项目配置 + conf = HttpFptConfig() + return conf - :param settings: 项目配置,字典或指定 toml 配置文件 - :param config_filename: - :return: - """ - global httpfpt_config - if isinstance(settings, str): - from httpfpt.common.toml_handler import read_toml +set_httpfpt_config = HttpFptConfig() - if not os.path.isdir(settings) and not os.path.isfile(settings): - raise ConfigInitError('运行失败,') - conf = read_toml(settings, config_filename) - else: - conf = settings - - httpfpt_config = HttpFptConfig(conf) - return httpfpt_config +# global config +httpfpt_config = set_httpfpt_config(httpfpt_path.settings_file_file) diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index e1b85ec..7a5bbff 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -4,39 +4,45 @@ import os +from typing_extensions import Self + from httpfpt.common.errors import ConfigInitError __all__ = [ - 'httpfpt_path', 'set_httpfpt_dir', + 'httpfpt_path', ] class HttpFptPathConfig: - def __init__(self, base_dir: str) -> None: - """ - 路径配置初始化 + def __init__(self) -> None: + self._base_dir = None - :param base_dir: - """ - self.base_dir = base_dir + def __call__(self, base_dir: str) -> Self: + self._base_dir = base_dir + global httpfpt_path + httpfpt_path = HttpFptPathConfig() + return httpfpt_path @property def project_dir(self) -> str: """项目根路径""" - if not self.base_dir or not os.path.exists(self.base_dir): - self.base_dir = os.getenv('HTTPFPT_PROJECT_PATH') - if not self.base_dir: - raise ConfigInitError( - '运行失败:在访问 httpfpt API 前,请先通过 set_project_dir() 方法设置项目根路径,' - '或配置 HTTPFPT_PROJECT_PATH 环境变量' - ) - return self.base_dir + if not self._base_dir: + self._base_dir = os.getenv('HTTPFPT_PROJECT_PATH') + if not self._base_dir: + raise ConfigInitError( + '运行失败:在访问 httpfpt API 前,请先通过 set_project_dir() 方法设置项目根路径,' + '或配置 HTTPFPT_PROJECT_PATH 环境变量' + ) + else: + if not os.path.exists(self._base_dir): + raise ConfigInitError(f'运行失败,项目路径 {self._base_dir} 不存在,请检查路径配置是否正确') + return self._base_dir @property def log_dir(self) -> str: """日志路径""" - if not self.base_dir or not os.path.exists(self.base_dir): + if not self._base_dir: return os.path.join(os.path.expanduser('~'), '.httpfpt') return os.path.join(self.project_dir, 'log') @@ -100,18 +106,13 @@ def auth_conf_dir(self) -> str: """AUTH配置文件路径""" return os.path.join(self.project_dir, 'core') + @property + def settings_file_file(self) -> str: + """核心配置文件路径""" + return os.path.join(self.project_dir, 'core', 'conf.toml') -def set_httpfpt_dir(base_dir: str) -> HttpFptPathConfig: - """ - 设置项目目录 - - :param base_dir: 项目根路径 - :return: - """ - global httpfpt_path - httpfpt_path = HttpFptPathConfig(base_dir) - return httpfpt_path +set_httpfpt_dir = HttpFptPathConfig() # global path_config diff --git a/httpfpt/run.py b/httpfpt/run.py index 01b0ec2..95ec469 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -19,9 +19,9 @@ from httpfpt.db.redis_db import redis_client from httpfpt.utils.case_auto_generator import auto_generate_testcases from httpfpt.utils.request import case_data_parse as case_data -from httpfpt.utils.send_report.ding_talk import DingTalk -from httpfpt.utils.send_report.lark_talk import LarkTalk -from httpfpt.utils.send_report.send_email import SendMail +from httpfpt.utils.send_report.ding import DingDing +from httpfpt.utils.send_report.email import SendEmail +from httpfpt.utils.send_report.feishu import FeiShu from httpfpt.utils.time_control import get_current_time @@ -130,14 +130,14 @@ def startup( yaml_report_files.sort() test_result = read_yaml(httpfpt_path.yaml_report_dir, filename=yaml_report_files[-1]) - if html_report and httpfpt_config.EMAIL_REPORT_SEND: - SendMail(test_result, html_report_filename).send_report() + if html_report and httpfpt_config.EMAIL_SEND: + SendEmail(test_result, html_report_filename).send_report() - if httpfpt_config.DING_TALK_REPORT_SEND: - DingTalk(test_result).send() + if httpfpt_config.DINGDING_SEND: + DingDing(test_result).send() - if httpfpt_config.LARK_TALK_REPORT_SEND: - LarkTalk(test_result).send() + if httpfpt_config.FEISHU_SEND: + FeiShu(test_result).send() if allure: if not os.path.exists(httpfpt_path.allure_report_env_file): @@ -233,4 +233,8 @@ def run( log.error(f'运行异常:{e}') import traceback - SendMail({'error': traceback.format_exc()}).send_error() + SendEmail({'error': traceback.format_exc()}).send_error() + + +if __name__ == '__main__': + run(testcase_generate=True) diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index 3f72e2b..08afb52 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -36,36 +36,32 @@ def create_new_project() -> None: shutil.copyfile(conftest, conftest_file) with import_path('httpfpt', 'pytest.ini') as pytest_ini: shutil.copyfile(pytest_ini, pytest_file) - run_settings_path = os.path.join(project_path, 'core', 'conf.toml') init_tpl = f"""#!/usr/bin/env python3 # -*- coding: utf-8 -*- from functools import wraps from typing import Any, Callable from httpfpt import set_httpfpt_dir -from httpfpt import set_httpfpt_config - # Init setup set_httpfpt_dir('{project_path}') -set_httpfpt_config('{run_settings_path}') def ensure_httpfpt_setup(func: Callable) -> Any: @wraps(func) def wrapper(*args, **kwargs) -> Callable: set_httpfpt_dir('{project_path}') - set_httpfpt_config('{run_settings_path}') return func(*args, **kwargs) return wrapper + """ run_tpl = """#!/usr/bin/env python3 # -*- coding: utf-8 -*- from httpfpt.run import run as httpfpt_run +httpfpt_run(testcase_generate=True) -httpfpt_run() """ with open(os.path.join(project_path, '__init__.py'), 'w', encoding='utf-8') as f: f.write(init_tpl) diff --git a/httpfpt/utils/send_report/ding_talk.py b/httpfpt/utils/send_report/ding.py similarity index 91% rename from httpfpt/utils/send_report/ding_talk.py rename to httpfpt/utils/send_report/ding.py index c4b7b6b..ee91514 100644 --- a/httpfpt/utils/send_report/ding_talk.py +++ b/httpfpt/utils/send_report/ding.py @@ -4,7 +4,7 @@ from httpfpt.core.get_conf import httpfpt_config -class DingTalk: +class DingDing: def __init__(self, content: dict): self.content = content @@ -30,10 +30,10 @@ def send(self) -> None: }, } response = requests.session().post( - url=httpfpt_config.DING_TALK_WEBHOOK, + url=httpfpt_config.DINGDING_WEBHOOK, json=data, headers=headers, - proxies=httpfpt_config.DING_TALK_PROXY, # type: ignore + proxies=httpfpt_config.DINGDING_PROXY, # type: ignore ) response.raise_for_status() except Exception as e: diff --git a/httpfpt/utils/send_report/send_email.py b/httpfpt/utils/send_report/email.py similarity index 99% rename from httpfpt/utils/send_report/send_email.py rename to httpfpt/utils/send_report/email.py index da35f0d..42fd47c 100644 --- a/httpfpt/utils/send_report/send_email.py +++ b/httpfpt/utils/send_report/email.py @@ -18,7 +18,7 @@ from httpfpt.utils.file_control import get_file_property -class SendMail: +class SendEmail: def __init__(self, content: dict, filename: str | None = None): self.content = content self.filename = filename diff --git a/httpfpt/utils/send_report/lark_talk.py b/httpfpt/utils/send_report/feishu.py similarity index 93% rename from httpfpt/utils/send_report/lark_talk.py rename to httpfpt/utils/send_report/feishu.py index 10279eb..df7e947 100644 --- a/httpfpt/utils/send_report/lark_talk.py +++ b/httpfpt/utils/send_report/feishu.py @@ -4,7 +4,7 @@ from httpfpt.core.get_conf import httpfpt_config -class LarkTalk: +class FeiShu: def __init__(self, content: dict): self.content = content @@ -36,10 +36,10 @@ def send(self) -> None: }, } response = requests.session().post( - url=httpfpt_config.LARK_TALK_WEBHOOK, + url=httpfpt_config.FEISHU_WEBHOOK, json=data, headers=headers, - proxies=httpfpt_config.LARK_TALK_PROXY, # type: ignore + proxies=httpfpt_config.FEISHU_PROXY, # type: ignore ) response.raise_for_status() except Exception as e: diff --git a/pdm.lock b/pdm.lock index 9037bf6..fac42c4 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "test"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:08a740389fea6f46200dc53b108ab00c998d58f827040d253fdda2de6c57fb1f" +content_hash = "sha256:8416836d7873917b152ce6dfbc0701c5a6a3f061698f15495fb96ac47d80cabb" [[package]] name = "allure-pytest" @@ -82,6 +82,15 @@ files = [ {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] +[[package]] +name = "boltons" +version = "23.1.1" +summary = "When they're not builtins, they're boltons." +files = [ + {file = "boltons-23.1.1-py2.py3-none-any.whl", hash = "sha256:80a8cd930ff21fbf03545b9863e5799d0c3e7e0e3b2546bdaf2efccd7b3708cc"}, + {file = "boltons-23.1.1.tar.gz", hash = "sha256:d2cb2fa83cf2ebe791be1e284183e8a43a1031355156a968f8e0a333ad2448fc"}, +] + [[package]] name = "cache3" version = "0.4.3" @@ -370,6 +379,18 @@ files = [ {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] +[[package]] +name = "face" +version = "20.1.1" +summary = "A command-line application framework (and CLI parser). Friendly for users, full-featured for developers." +dependencies = [ + "boltons>=20.0.0", +] +files = [ + {file = "face-20.1.1-py3-none-any.whl", hash = "sha256:ca3a1d8b8b6aa8e61d62a300e9ee24e09c062aceda549e9a640128e4fa0f4559"}, + {file = "face-20.1.1.tar.gz", hash = "sha256:7d59ca5ba341316e58cf72c6aff85cca2541cf5056c4af45cb63af9a814bed3e"}, +] + [[package]] name = "faker" version = "24.0.0" @@ -394,6 +415,20 @@ files = [ {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, ] +[[package]] +name = "glom" +version = "23.5.0" +summary = "A declarative object transformer and formatter, for conglomerating nested data." +dependencies = [ + "attrs", + "boltons>=19.3.0", + "face==20.1.1", +] +files = [ + {file = "glom-23.5.0-py3-none-any.whl", hash = "sha256:fe4e9be4dc93c11a99f8277042e4bee95419c02cda4b969f504508b0a1aa6a66"}, + {file = "glom-23.5.0.tar.gz", hash = "sha256:06af5e3486aacc59382ba34e53ebeabd7a9345d78f7dbcbee26f03baa4b83bac"}, +] + [[package]] name = "h11" version = "0.12.0" diff --git a/pyproject.toml b/pyproject.toml index 35c9ff8..45f0c4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "stamina==24.1.0", "jsonschema>=4.21.1", "pygments>=2.17.2", + "glom>=23.5.0", ] requires-python = ">=3.8" readme = "README.md" @@ -52,6 +53,16 @@ test = [ "ruff>=0.2.0", ] +[tool.pdm.build] +excludes = [ + "**/__pycache__", + "**/.git", + "**/.pytest_cache", + "**/.ruff_cache", + "**/log", + "**/report" +] + [project.scripts] httpfpt = "httpfpt.cli:cappa_invoke" diff --git a/requirements.txt b/requirements.txt index 1e012b7..ba3884b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ annotated-types==0.6.0 anyio==3.7.1 async-timeout==4.0.3; python_full_version <= "3.11.2" attrs==23.2.0 +boltons==23.1.1 cache3==0.4.3 cappa==0.17.1 certifi==2023.11.17 @@ -20,8 +21,10 @@ dirty-equals==0.7.1 distlib==0.3.8 eval-type-backport==0.1.3 exceptiongroup==1.2.0; python_version < "3.11" +face==20.1.1 faker==24.0.0 filelock==3.12.4 +glom==23.5.0 h11==0.12.0 hiredis==2.3.2 httpcore==0.15.0 From 2bebd4a256ac221123cf2ccc7b9e0e73b25af5ec Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 9 Mar 2024 03:03:53 +0800 Subject: [PATCH 27/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=92=8C=E8=AE=BE=E7=BD=AE=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/common/toml_handler.py | 4 +- httpfpt/core/get_conf.py | 68 +++------------------------------- httpfpt/core/path_conf.py | 9 +---- httpfpt/run.py | 1 + 4 files changed, 9 insertions(+), 73 deletions(-) diff --git a/httpfpt/common/toml_handler.py b/httpfpt/common/toml_handler.py index 6dd0055..490a8fe 100644 --- a/httpfpt/common/toml_handler.py +++ b/httpfpt/common/toml_handler.py @@ -23,8 +23,8 @@ def read_toml(filepath: str, filename: str | None = None, encoding: str = 'utf-8 try: with open(filepath, encoding=encoding) as f: data = rtoml.load(f) - except ValueError as e: - log.error(f'文件 {filename} 内容在格式错误 \n {e}') + except Exception as e: + log.error(f'文件 {filename} 读取错误: {e}') raise e else: return data diff --git a/httpfpt/core/get_conf.py b/httpfpt/core/get_conf.py index 27d9ecb..7e9986c 100644 --- a/httpfpt/core/get_conf.py +++ b/httpfpt/core/get_conf.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- from __future__ import annotations -import os.path - from glom import glom from httpfpt.common.errors import ConfigInitError @@ -13,65 +11,10 @@ class HttpFptConfig: - def __init__(self) -> None: - # 项目目录名 - self.PROJECT_NAME = None - # 测试报告 - self.TEST_REPORT_TITLE = None - self.TESTER_NAME = None - # mysql 数据库 - self.MYSQL_HOST = None - self.MYSQL_PORT = None - self.MYSQL_USER = None - self.MYSQL_PASSWORD = None - self.MYSQL_DATABASE = None - self.MYSQL_CHARSET = None - # redis 数据库 - self.REDIS_HOST = None - self.REDIS_PORT = None - self.REDIS_PASSWORD = None - self.REDIS_DATABASE = None - self.REDIS_TIMEOUT = None - # 邮件 - self.EMAIL_SERVER = None - self.EMAIL_PORT = None - self.EMAIL_USER = None - self.EMAIL_PASSWORD = None - self.EMAIL_SEND_TO = None - self.EMAIL_SSL = None - self.EMAIL_SEND = None - # 钉钉 - self.DINGDING_WEBHOOK = None - self.DINGDING_PROXY = None - self.DINGDING_SEND = None - # 飞书 - self.FEISHU_WEBHOOK = None - self.FEISHU_PROXY = None - self.FEISHU_SEND = None - # 请求发送 - self.REQUEST_TIMEOUT = None - self.REQUEST_VERIFY = None - self.REQUEST_REDIRECTS = None - self.REQUEST_PROXIES_REQUESTS = None - self.REQUEST_PROXIES_HTTPX = None - self.REQUEST_RETRY = None - - def __call__(self, settings: str | dict, config_filename: str | None = None) -> HttpFptConfig: - """ - 设置项目配置 - - :param settings: 项目配置,字典或指定 toml 配置文件 - :param config_filename: - :return: - """ - if isinstance(settings, str): - from httpfpt.common.toml_handler import read_toml + def __call__(self) -> HttpFptConfig: + from httpfpt.common.toml_handler import read_toml - if not os.path.isdir(settings) and not os.path.isfile(settings): - raise ConfigInitError('配置获取失败,请检查配置文件路径是否合法') - self.settings = read_toml(settings, config_filename) - else: - self.settings = settings + self.settings = read_toml(httpfpt_path.settings_file_file) try: self.PROJECT_NAME = glom(self.settings, 'project.name') self.TEST_REPORT_TITLE = glom(self.settings, 'report.title') @@ -137,11 +80,10 @@ def __call__(self, settings: str | dict, config_filename: str | None = None) -> except KeyError as e: raise ConfigInitError(f'配置解析失败:缺失参数 {str(e)},请核对配置文件或字典') - conf = HttpFptConfig() - return conf + return self set_httpfpt_config = HttpFptConfig() # global config -httpfpt_config = set_httpfpt_config(httpfpt_path.settings_file_file) +httpfpt_config = set_httpfpt_config() diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index 7a5bbff..f0d006b 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -15,14 +15,9 @@ class HttpFptPathConfig: - def __init__(self) -> None: - self._base_dir = None - def __call__(self, base_dir: str) -> Self: self._base_dir = base_dir - global httpfpt_path - httpfpt_path = HttpFptPathConfig() - return httpfpt_path + return self @property def project_dir(self) -> str: @@ -42,8 +37,6 @@ def project_dir(self) -> str: @property def log_dir(self) -> str: """日志路径""" - if not self._base_dir: - return os.path.join(os.path.expanduser('~'), '.httpfpt') return os.path.join(self.project_dir, 'log') @property diff --git a/httpfpt/run.py b/httpfpt/run.py index 95ec469..df58a2c 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -236,5 +236,6 @@ def run( SendEmail({'error': traceback.format_exc()}).send_error() +# DEBUG for SDK if __name__ == '__main__': run(testcase_generate=True) From 3baa4729ae478b392fbc11f33e88e490dd1d897b Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 9 Mar 2024 19:43:09 +0800 Subject: [PATCH 28/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/common/send_request.py | 8 +++++- pdm.lock | 50 +++++++++++++++++----------------- requirements.txt | 6 ++-- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/httpfpt/common/send_request.py b/httpfpt/common/send_request.py index 07e872b..e7abfbb 100644 --- a/httpfpt/common/send_request.py +++ b/httpfpt/common/send_request.py @@ -361,7 +361,13 @@ def allure_request_teardown(teardown_log: dict) -> None: @staticmethod def allure_request_down(response_data: dict) -> None: - allure_step('响应数据', {'status_code': response_data['status_code'], 'elapsed': response_data['elapsed']}) + allure_step( + '响应数据', + { + 'status_code': response_data['status_code'], + 'elapsed': response_data['elapsed'], + }, + ) @staticmethod def allure_dynamic_data(parsed_data: dict) -> None: diff --git a/pdm.lock b/pdm.lock index fac42c4..916a34f 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "test"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:8416836d7873917b152ce6dfbc0701c5a6a3f061698f15495fb96ac47d80cabb" +content_hash = "sha256:248015b4d43b74a109b81c00bffdd1535e6d0f4bcbbaeb54cbf5b2d227bad1d3" [[package]] name = "allure-pytest" @@ -393,7 +393,7 @@ files = [ [[package]] name = "faker" -version = "24.0.0" +version = "24.1.0" requires_python = ">=3.8" summary = "Faker is a Python package that generates fake data for you." dependencies = [ @@ -401,8 +401,8 @@ dependencies = [ "typing-extensions>=3.10.0.1; python_version <= \"3.8\"", ] files = [ - {file = "Faker-24.0.0-py3-none-any.whl", hash = "sha256:2456d674f40bd51eb3acbf85221277027822e529a90cc826453d9a25dff932b1"}, - {file = "Faker-24.0.0.tar.gz", hash = "sha256:ea6f784c40730de0f77067e49e78cdd590efb00bec3d33f577492262206c17fc"}, + {file = "Faker-24.1.0-py3-none-any.whl", hash = "sha256:89ae0932f4f269754790569828859eaa0ae2ce73d1f3eb1f30ae7c20d4daf5ce"}, + {file = "Faker-24.1.0.tar.gz", hash = "sha256:4fb0c16c71ad35d278a5fa7a4106a5c26c2b2b5c5efc47c1d67635db90b6071e"}, ] [[package]] @@ -1081,12 +1081,12 @@ files = [ [[package]] name = "python-jsonpath" -version = "1.0.0" +version = "1.1.0" requires_python = ">=3.7" summary = "JSONPath, JSON Pointer and JSON Patch for Python." files = [ - {file = "python_jsonpath-1.0.0-py3-none-any.whl", hash = "sha256:683df06e2fb95e2efb5f5b970a4e7468e644029bad3df69515b7c3494635bbff"}, - {file = "python_jsonpath-1.0.0.tar.gz", hash = "sha256:17f48bc766ad362d0613521f50f5568dd4c46687362582035a5bf4e19f30d71d"}, + {file = "python_jsonpath-1.1.0-py3-none-any.whl", hash = "sha256:a25827723d4638e77db497ffcc05c8f1ae077b8fbf0def3b6c9466819d7716b6"}, + {file = "python_jsonpath-1.1.0.tar.gz", hash = "sha256:e32dfe5d6d940f62516c611cc33492662f4249f9a93fc51822fcb06914bf0752"}, ] [[package]] @@ -1399,27 +1399,27 @@ files = [ [[package]] name = "ruff" -version = "0.3.0" +version = "0.3.2" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944"}, - {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a"}, - {file = "ruff-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2"}, - {file = "ruff-0.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f"}, - {file = "ruff-0.3.0-py3-none-win32.whl", hash = "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b"}, - {file = "ruff-0.3.0-py3-none-win_amd64.whl", hash = "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f"}, - {file = "ruff-0.3.0-py3-none-win_arm64.whl", hash = "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"}, - {file = "ruff-0.3.0.tar.gz", hash = "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a"}, + {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"}, + {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"}, + {file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"}, + {file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"}, + {file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"}, + {file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"}, + {file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"}, + {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, ] [[package]] diff --git a/requirements.txt b/requirements.txt index ba3884b..b6d34fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ distlib==0.3.8 eval-type-backport==0.1.3 exceptiongroup==1.2.0; python_version < "3.11" face==20.1.1 -faker==24.0.0 +faker==24.1.0 filelock==3.12.4 glom==23.5.0 h11==0.12.0 @@ -60,7 +60,7 @@ pytest-metadata==3.0.0 pytest-pretty==1.2.0 python-dateutil==2.8.2 python-dotenv==0.20.0 -python-jsonpath==1.0.0 +python-jsonpath==1.1.0 pytz==2023.3.post1 pyyaml==6.0.1 redis==5.0.1 @@ -70,7 +70,7 @@ rfc3986==1.5.0 rich==13.6.0 rpds-py==0.18.0 rtoml==0.9.0 -ruff==0.3.0 +ruff==0.3.2 setuptools==69.0.3 six==1.16.0 sniffio==1.3.0 From 1c2b76e77f349ecc5519904fa51215505287391a Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 9 Mar 2024 20:12:49 +0800 Subject: [PATCH 29/46] =?UTF-8?q?=E9=87=8D=E5=91=BD=E5=90=8Dding=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/run.py | 2 +- httpfpt/utils/send_report/{ding.py => dingding.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename httpfpt/utils/send_report/{ding.py => dingding.py} (100%) diff --git a/httpfpt/run.py b/httpfpt/run.py index df58a2c..bf2b9ee 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -19,7 +19,7 @@ from httpfpt.db.redis_db import redis_client from httpfpt.utils.case_auto_generator import auto_generate_testcases from httpfpt.utils.request import case_data_parse as case_data -from httpfpt.utils.send_report.ding import DingDing +from httpfpt.utils.send_report.dingding import DingDing from httpfpt.utils.send_report.email import SendEmail from httpfpt.utils.send_report.feishu import FeiShu from httpfpt.utils.time_control import get_current_time diff --git a/httpfpt/utils/send_report/ding.py b/httpfpt/utils/send_report/dingding.py similarity index 100% rename from httpfpt/utils/send_report/ding.py rename to httpfpt/utils/send_report/dingding.py From 45134bb153c300a1ae0343833e8dbb5eebd28db3 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 9 Mar 2024 20:17:33 +0800 Subject: [PATCH 30/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/core/get_conf.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/httpfpt/core/get_conf.py b/httpfpt/core/get_conf.py index 7e9986c..9551537 100644 --- a/httpfpt/core/get_conf.py +++ b/httpfpt/core/get_conf.py @@ -37,26 +37,26 @@ def __call__(self) -> HttpFptConfig: self.EMAIL_SEND_TO = glom(self.settings, 'email.receiver') self.EMAIL_SSL = glom(self.settings, 'email.ssl') self.EMAIL_SEND = glom(self.settings, 'email.send') - self.DINGDING_WEBHOOK = glom(self.settings, 'ding.webhook') + self.DINGDING_WEBHOOK = glom(self.settings, 'dingding.webhook') self.DINGDING_PROXY = { - 'http': glom(self.settings, 'ding.proxies.http') - if glom(self.settings, 'ding.proxies.http') != '' + 'http': glom(self.settings, 'dingding.proxies.http') + if glom(self.settings, 'dingding.proxies.http') != '' else None, - 'https': glom(self.settings, 'ding.proxies.https') - if glom(self.settings, 'ding.proxies.https') != '' + 'https': glom(self.settings, 'dingding.proxies.https') + if glom(self.settings, 'dingding.proxies.https') != '' else None, } - self.DINGDING_SEND = glom(self.settings, 'ding.send') - self.FEISHU_WEBHOOK = glom(self.settings, 'lark.webhook') + self.DINGDING_SEND = glom(self.settings, 'dingding.send') + self.FEISHU_WEBHOOK = glom(self.settings, 'feishu.webhook') self.FEISHU_PROXY = { - 'http': glom(self.settings, 'lark.proxies.http') - if glom(self.settings, 'lark.proxies.http') != '' + 'http': glom(self.settings, 'feishu.proxies.http') + if glom(self.settings, 'feishu.proxies.http') != '' else None, - 'https': glom(self.settings, 'lark.proxies.https') - if glom(self.settings, 'lark.proxies.https') != '' + 'https': glom(self.settings, 'feishu.proxies.https') + if glom(self.settings, 'feishu.proxies.https') != '' else None, } - self.FEISHU_SEND = glom(self.settings, 'lark.send') + self.FEISHU_SEND = glom(self.settings, 'feishu.send') self.REQUEST_TIMEOUT = glom(self.settings, 'request.timeout') self.REQUEST_VERIFY = glom(self.settings, 'request.verify') self.REQUEST_REDIRECTS = glom(self.settings, 'request.redirects') From 752f4dccd2d8da6d851a417500462929bd7cd73b Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 9 Mar 2024 20:27:35 +0800 Subject: [PATCH 31/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=88=9B=E5=BB=BA=E6=B5=8B=E8=AF=95=E7=94=A8?= =?UTF-8?q?=E4=BE=8B=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpfpt/run.py b/httpfpt/run.py index bf2b9ee..b9eb63f 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -43,7 +43,7 @@ def startup( ) -> None: """运行启动程序""" if testcase_generate: - auto_generate_testcases(testcase_generate) + auto_generate_testcases() run_args = [log_level] @@ -178,7 +178,7 @@ def run( 运行入口 :param args: pytest 运行参数 - :param testcase_generate: 自动生成测试用例(危险行为,自动强制覆盖同名文件),建议通过 CLI 手动执行,默认关闭 + :param testcase_generate: 自动生成测试用例(跳过同名文件),建议通过 CLI 手动执行,默认关闭 :param clean_cache: 清理 redis 缓存数据,对于脏数据,这很有用,默认关闭 :param pydantic_verify: 用例数据完整架构 pydantic 快速检测, 默认开启 :param args: pytest 运行参数 From 10999c442b0cda6e092c49fcdf4afd68a791a32b Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 9 Mar 2024 20:41:30 +0800 Subject: [PATCH 32/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0README=E8=AF=B4?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dd06d7d..08909c2 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ ![GitHub release (with filter)](https://img.shields.io/github/v/release/wu-clan/httpfpt) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +> [!IMPORTANT] +> 当前分支为 SDK 版本,如需修改源码进行功能定制,请切换到 [master](https://github.com/wu-clan/httpfpt) 分支,以获取更简易的本地定制化 + 基于 HTTP 请求的快速数据驱动 pytest 接口自动化测试框架 我在掘金发表了关于 `HttpFpt` 的前身和由来,包括部分功能点的说明, 感兴趣 @@ -66,9 +69,10 @@ git clone https://github.com/wu-clan/httpfpt.git [Docker](https://hub.docker.com/_/mysql) > [!WARNING] -> allure 测试报告默认使用 allure-pytest -> 生成,但是不能直接访问,你必须安装 [allure](https://www.yuque.com/poloyy/python/aiqlmi) -> 本地程序和 [Java JDK](https://adoptopenjdk.net/archive.html?variant=openjdk8&jvmVariant=hotspot) 才能进行可视化浏览 +> allure 测试报告默认使用 allure-pytest 生成,但是不能直接访问,有以下选择 +> 1. 本地访问:你必须安装 [allure](https://www.yuque.com/poloyy/python/aiqlmi) + 程序和 [Java JDK](https://adoptopenjdk.net/archive.html?variant=openjdk8&jvmVariant=hotspot) 才能进行本地可视化浏览 +> 2. Jenkins(文档内包含集成教程): 将 allure 测试报告集成到到 Jenkins 中,通过 Jenkins 进行浏览 ## 帮助 From 565a9459f3724e3d1b27e222a980a9aec87c52fa Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 9 Mar 2024 22:20:58 +0800 Subject: [PATCH 33/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dlint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 4 ++-- httpfpt/common/json_handler.py | 8 +++++++- httpfpt/common/send_request.py | 4 +++- httpfpt/run.py | 2 +- httpfpt/utils/relate_testcase_executor.py | 4 +++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index 1524e81..9eef000 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -13,7 +13,7 @@ from rich.traceback import install as rich_install from typing_extensions import Annotated -sys.path.append(os.path.dirname(os.path.dirname(__file__))) +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from httpfpt.utils.cli.about_testcase import generate_testcases, testcase_data_verify from httpfpt.utils.cli.import_case_data import ( @@ -69,7 +69,7 @@ class TestCaseCLI: short='-c', long=True, default='', - help='验证测试数据结构;当指定文件(文件名/完整路径)时, 仅验证指定文件, 当指定 "all" 时, 验证所有文件.', + help='验证测试数据结构;当指定文件(文件名/绝对路径)时, 仅验证指定文件, 当指定 "all" 时, 验证所有文件.', required=False, ), ] diff --git a/httpfpt/common/json_handler.py b/httpfpt/common/json_handler.py index c36b9ef..bb1fb26 100644 --- a/httpfpt/common/json_handler.py +++ b/httpfpt/common/json_handler.py @@ -35,7 +35,13 @@ def read_json_file(filepath: str, filename: str | None = None, **kwargs) -> dict def write_json_file( - filepath: str | None = None, *, filename: str, data: Any = None, encoding: str = 'utf-8', mode: str = 'a', **kwargs + filepath: str | None = None, + *, + filename: str, + data: Any = None, + encoding: str = 'utf-8', + mode: str = 'a', + **kwargs, ) -> None: """ 写入 json 文件 diff --git a/httpfpt/common/send_request.py b/httpfpt/common/send_request.py index e7abfbb..9997bb5 100644 --- a/httpfpt/common/send_request.py +++ b/httpfpt/common/send_request.py @@ -48,7 +48,9 @@ def init_response_metadata(self) -> dict: 'json': None, 'content': None, 'text': None, - 'stat': {'execute_time': None}, + 'stat': { + 'execute_time': None, + }, } return response_metadata diff --git a/httpfpt/run.py b/httpfpt/run.py index b9eb63f..cad127f 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -10,7 +10,7 @@ import pytest -sys.path.append(os.path.dirname(os.path.dirname(__file__))) +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from httpfpt.common.log import log from httpfpt.common.yaml_handler import read_yaml diff --git a/httpfpt/utils/relate_testcase_executor.py b/httpfpt/utils/relate_testcase_executor.py index d86c77f..eddd51f 100644 --- a/httpfpt/utils/relate_testcase_executor.py +++ b/httpfpt/utils/relate_testcase_executor.py @@ -68,7 +68,9 @@ def exec_setup_testcase(parsed_data: dict, setup_testcase: str | dict) -> dict | response = relate_testcase_exec_with_new_request_data(case_data) # 使用更新请求数据后的请求响应提取变量 if setup_testcase.get('response') is not None: - testcase_data = {'set_var_response': setup_testcase['response']} + testcase_data = { + 'set_var_response': setup_testcase['response'], + } relate_testcase_extract_with_response(testcase_data, response) else: if setup_testcase.get('response') is not None: From 0f30bb443cce56a6a62910c51bba8f9d8ef8fa47 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 10 Mar 2024 18:56:49 +0800 Subject: [PATCH 34/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E8=A7=A6=E5=8F=91=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 21 ++++++++++++--------- httpfpt/core/path_conf.py | 15 +++++++-------- httpfpt/run.py | 5 ----- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index 9eef000..4dc6287 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -15,15 +15,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from httpfpt.utils.cli.about_testcase import generate_testcases, testcase_data_verify -from httpfpt.utils.cli.import_case_data import ( - import_apifox_case_data, - import_git_case_data, - import_har_case_data, - import_jmeter_case_data, - import_openapi_case_data, - import_postman_case_data, -) + from httpfpt.utils.cli.new_project import create_new_project from httpfpt.utils.cli.version import get_version @@ -85,6 +77,8 @@ class TestCaseCLI: ] def __call__(self) -> None: + from httpfpt.utils.cli.about_testcase import generate_testcases, testcase_data_verify + if self.data_verify: testcase_data_verify(self.data_verify) if self.generate: @@ -158,6 +152,15 @@ class ImportCLI: ] def __call__(self) -> None: + from httpfpt.utils.cli.import_case_data import ( + import_apifox_case_data, + import_git_case_data, + import_har_case_data, + import_jmeter_case_data, + import_openapi_case_data, + import_postman_case_data, + ) + if self.openai: import_openapi_case_data(self.openai) if self.apifox: diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index f0d006b..02e6f6c 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -22,13 +22,12 @@ def __call__(self, base_dir: str) -> Self: @property def project_dir(self) -> str: """项目根路径""" - if not self._base_dir: - self._base_dir = os.getenv('HTTPFPT_PROJECT_PATH') - if not self._base_dir: - raise ConfigInitError( - '运行失败:在访问 httpfpt API 前,请先通过 set_project_dir() 方法设置项目根路径,' - '或配置 HTTPFPT_PROJECT_PATH 环境变量' - ) + _base_dir = self._base_dir if os.path.isdir(self._base_dir) else os.getenv('HTTPFPT_PROJECT_PATH') + if not _base_dir: + raise ConfigInitError( + '运行失败:在访问 HTTPFPT API 前,请先通过 `httpfpt` 命令创建新项目;' + '如果已经创建,请确保配置了 `HTTPFPT_PROJECT_PATH` 环境变量为项目目录' + ) else: if not os.path.exists(self._base_dir): raise ConfigInitError(f'运行失败,项目路径 {self._base_dir} 不存在,请检查路径配置是否正确') @@ -109,4 +108,4 @@ def settings_file_file(self) -> str: # global path_config -httpfpt_path: HttpFptPathConfig = set_httpfpt_dir('') +httpfpt_path = set_httpfpt_dir('') diff --git a/httpfpt/run.py b/httpfpt/run.py index cad127f..5503dd3 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -234,8 +234,3 @@ def run( import traceback SendEmail({'error': traceback.format_exc()}).send_error() - - -# DEBUG for SDK -if __name__ == '__main__': - run(testcase_generate=True) From 4251dcd3ffe2add8aa1510c31a64e36e992aa652 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 10 Mar 2024 19:11:13 +0800 Subject: [PATCH 35/46] =?UTF-8?q?=E6=B7=BB=E5=8A=A0typed=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 httpfpt/py.typed diff --git a/httpfpt/py.typed b/httpfpt/py.typed new file mode 100644 index 0000000..e69de29 From 8dc6bb7b3228ae921400b90ee4c81ad58163cc2f Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 10 Mar 2024 23:19:44 +0800 Subject: [PATCH 36/46] =?UTF-8?q?=E4=B8=BA=E5=8F=91=E5=B8=83pypi=E5=8C=85?= =?UTF-8?q?=E5=81=9A=E5=87=86=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 50022aa..3eb4f23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,10 +41,23 @@ dependencies = [ "pygments>=2.17.2", "glom>=23.5.0", ] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] requires-python = ">=3.8" readme = "README.md" license = {text = "MIT"} +[project.urls] +homepage = "https://github.com/wu-clan/httpfpt" +repository = "https://github.com/wu-clan/httpfpt" + [tool.pdm] version = { source = "file", path = "httpfpt/__init__.py" } @@ -56,12 +69,12 @@ test = [ [tool.pdm.build] excludes = [ - "**/__pycache__", - "**/.git", - "**/.pytest_cache", - "**/.ruff_cache", - "**/log", - "**/report" + "**/__pycache__/", + "**/.git/", + "**/.pytest_cache/", + "**/.ruff_cache/", + "**/log/", + "**/report/" ] [project.scripts] From 2c185ff4e4221c9faa739dd5f043be1730b7939a Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 10 Mar 2024 23:28:31 +0800 Subject: [PATCH 37/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AEcli=E6=8E=A7=E5=88=B6=E5=8F=B0?= =?UTF-8?q?=E6=89=93=E5=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/utils/cli/new_project.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index 08afb52..da89146 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -17,6 +17,7 @@ def create_new_project() -> None: name = Prompt.ask('❓ Set your project a name', default='httpfpt_project') path = Prompt.ask('❓ Set your project path (relative or absolute path, which automatically parses.)', default='.') + console.print('\n⏳ The project is being created automatically...') if path != '.': if not os.path.isdir(path): raise cappa.Exit(f'\n❌ The "{path}" is not a directory', code=1) @@ -70,4 +71,6 @@ def wrapper(*args, **kwargs) -> Callable: console.print( f'\n🎉 The project "{name}" has been created.' f'\n🌴 The project is located in the directory: [cyan]{project_path}[/]' + f'\n⚠️ Before accessing HTTPFPT, be sure to set the environment variable ' + f'HTTPFPT_PROJECT_PATH to the current project directory' ) From 202db575874a40236e1848777f8ca138f467bc31 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sun, 10 Mar 2024 23:31:26 +0800 Subject: [PATCH 38/46] TODO --- httpfpt/utils/cli/new_project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index da89146..4a7148a 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -21,6 +21,7 @@ def create_new_project() -> None: if path != '.': if not os.path.isdir(path): raise cappa.Exit(f'\n❌ The "{path}" is not a directory', code=1) + # TODO: 添加 rich 创建过程进度打印 project_path = os.path.abspath(os.sep.join([path, name])) core_path = os.path.join(project_path, 'core') data_path = os.path.join(project_path, 'data') From 324cc07111d62f520b1a06deaf31074f22d8e20e Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 11 Mar 2024 00:21:19 +0800 Subject: [PATCH 39/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/__init__.py | 1 - httpfpt/core/path_conf.py | 24 ++++++----------------- httpfpt/run.py | 2 +- httpfpt/utils/cli/new_project.py | 33 +++++++++----------------------- 4 files changed, 16 insertions(+), 44 deletions(-) diff --git a/httpfpt/__init__.py b/httpfpt/__init__.py index 34386f8..0fc9d1a 100644 --- a/httpfpt/__init__.py +++ b/httpfpt/__init__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from httpfpt.core.path_conf import set_httpfpt_dir as set_httpfpt_dir __version__ = 'v0.6.0' diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index 02e6f6c..1b13799 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -4,34 +4,25 @@ import os -from typing_extensions import Self - from httpfpt.common.errors import ConfigInitError -__all__ = [ - 'set_httpfpt_dir', - 'httpfpt_path', -] +__all__ = ['httpfpt_path'] class HttpFptPathConfig: - def __call__(self, base_dir: str) -> Self: - self._base_dir = base_dir - return self - @property def project_dir(self) -> str: """项目根路径""" - _base_dir = self._base_dir if os.path.isdir(self._base_dir) else os.getenv('HTTPFPT_PROJECT_PATH') + _base_dir = os.getenv('HTTPFPT_PROJECT_PATH') if not _base_dir: raise ConfigInitError( '运行失败:在访问 HTTPFPT API 前,请先通过 `httpfpt` 命令创建新项目;' '如果已经创建,请确保配置了 `HTTPFPT_PROJECT_PATH` 环境变量为项目目录' ) else: - if not os.path.exists(self._base_dir): - raise ConfigInitError(f'运行失败,项目路径 {self._base_dir} 不存在,请检查路径配置是否正确') - return self._base_dir + if not os.path.exists(_base_dir): + raise ConfigInitError(f'运行失败,项目路径 {_base_dir} 不存在,请检查环境变量配置是否正确') + return _base_dir @property def log_dir(self) -> str: @@ -104,8 +95,5 @@ def settings_file_file(self) -> str: return os.path.join(self.project_dir, 'core', 'conf.toml') -set_httpfpt_dir = HttpFptPathConfig() - - # global path_config -httpfpt_path = set_httpfpt_dir('') +httpfpt_path = HttpFptPathConfig() diff --git a/httpfpt/run.py b/httpfpt/run.py index 5503dd3..19426d4 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -47,7 +47,7 @@ def startup( run_args = [log_level] - default_case_path = os.sep.join([os.path.dirname(__file__), 'testcases', httpfpt_config.PROJECT_NAME]) + default_case_path = os.sep.join([httpfpt_path.testcase_dir, httpfpt_config.PROJECT_NAME]) if case_path: if '::' not in case_path: raise ValueError( diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index 4a7148a..e80aa69 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -25,39 +25,23 @@ def create_new_project() -> None: project_path = os.path.abspath(os.sep.join([path, name])) core_path = os.path.join(project_path, 'core') data_path = os.path.join(project_path, 'data') + init_file = os.path.join(project_path, '__init__.py') conftest_file = os.path.join(project_path, 'conftest.py') pytest_file = os.path.join(project_path, 'pytest.ini') if os.path.exists(project_path): raise cappa.Exit(f'\n❌ The "{name}" directory is not empty', code=1) os.makedirs(project_path) with import_path('httpfpt.core', '') as core_data: - shutil.copytree(core_data, core_path, ignore=shutil.ignore_patterns('get_conf.py', 'path_conf.py')) + patterns = ['__init__.py', 'get_conf.py', 'path_conf.py'] + shutil.copytree(core_data, core_path, ignore=shutil.ignore_patterns(*patterns)) with import_path('httpfpt.data', '') as case_data: shutil.copytree(case_data, data_path) + with import_path('httpfpt', '__init__.py') as pytest_init: + shutil.copyfile(pytest_init, init_file) with import_path('httpfpt', 'conftest.py') as conftest: shutil.copyfile(conftest, conftest_file) with import_path('httpfpt', 'pytest.ini') as pytest_ini: shutil.copyfile(pytest_ini, pytest_file) - init_tpl = f"""#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from functools import wraps -from typing import Any, Callable - -from httpfpt import set_httpfpt_dir - -# Init setup -set_httpfpt_dir('{project_path}') - - -def ensure_httpfpt_setup(func: Callable) -> Any: - @wraps(func) - def wrapper(*args, **kwargs) -> Callable: - set_httpfpt_dir('{project_path}') - return func(*args, **kwargs) - - return wrapper - -""" run_tpl = """#!/usr/bin/env python3 # -*- coding: utf-8 -*- from httpfpt.run import run as httpfpt_run @@ -65,13 +49,14 @@ def wrapper(*args, **kwargs) -> Callable: httpfpt_run(testcase_generate=True) """ - with open(os.path.join(project_path, '__init__.py'), 'w', encoding='utf-8') as f: - f.write(init_tpl) with open(os.path.join(project_path, 'run.py'), 'w', encoding='utf-8') as f: f.write(run_tpl) console.print( f'\n🎉 The project "{name}" has been created.' f'\n🌴 The project is located in the directory: [cyan]{project_path}[/]' f'\n⚠️ Before accessing HTTPFPT, be sure to set the environment variable ' - f'HTTPFPT_PROJECT_PATH to the current project directory' + f'[yellow]HTTPFPT_PROJECT_PATH[/] to the current project directory' + f'\n Windows: setx HTTPFPT_PROJECT_PATH "{project_path}"' + f'\n Unix: vim ~/.bashrc' + f'\n\t export PATH=$PATH:{project_path}' ) From 05d3ffdc0f587052e4a61cf7d01b4dd2b229cab7 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 11 Mar 2024 16:00:30 +0800 Subject: [PATCH 40/46] =?UTF-8?q?=E4=B8=BA=E7=89=88=E6=9C=AC0.6.0=E5=81=9A?= =?UTF-8?q?=E5=87=86=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/core/get_conf.py | 27 +++++++++++++++++++++++++++ httpfpt/run.py | 18 ++++++++++++------ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/httpfpt/core/get_conf.py b/httpfpt/core/get_conf.py index 9551537..70a93cb 100644 --- a/httpfpt/core/get_conf.py +++ b/httpfpt/core/get_conf.py @@ -16,20 +16,29 @@ def __call__(self) -> HttpFptConfig: self.settings = read_toml(httpfpt_path.settings_file_file) try: + # 项目目录名 self.PROJECT_NAME = glom(self.settings, 'project.name') + + # 测试报告 self.TEST_REPORT_TITLE = glom(self.settings, 'report.title') self.TESTER_NAME = glom(self.settings, 'report.tester_name') + + # mysql 数据库 self.MYSQL_HOST = glom(self.settings, 'mysql.host') self.MYSQL_PORT = glom(self.settings, 'mysql.port') self.MYSQL_USER = glom(self.settings, 'mysql.user') self.MYSQL_PASSWORD = glom(self.settings, 'mysql.password') self.MYSQL_DATABASE = glom(self.settings, 'mysql.database') self.MYSQL_CHARSET = glom(self.settings, 'mysql.charset') + + # redis 数据库 self.REDIS_HOST = glom(self.settings, 'redis.host') self.REDIS_PORT = glom(self.settings, 'redis.port') self.REDIS_PASSWORD = glom(self.settings, 'redis.password') self.REDIS_DATABASE = glom(self.settings, 'redis.database') self.REDIS_TIMEOUT = glom(self.settings, 'redis.timeout') + + # 邮件 self.EMAIL_SERVER = glom(self.settings, 'email.host') self.EMAIL_PORT = glom(self.settings, 'email.port') self.EMAIL_USER = glom(self.settings, 'email.user') @@ -37,6 +46,8 @@ def __call__(self) -> HttpFptConfig: self.EMAIL_SEND_TO = glom(self.settings, 'email.receiver') self.EMAIL_SSL = glom(self.settings, 'email.ssl') self.EMAIL_SEND = glom(self.settings, 'email.send') + + # 钉钉 self.DINGDING_WEBHOOK = glom(self.settings, 'dingding.webhook') self.DINGDING_PROXY = { 'http': glom(self.settings, 'dingding.proxies.http') @@ -47,6 +58,8 @@ def __call__(self) -> HttpFptConfig: else None, } self.DINGDING_SEND = glom(self.settings, 'dingding.send') + + # 飞书 self.FEISHU_WEBHOOK = glom(self.settings, 'feishu.webhook') self.FEISHU_PROXY = { 'http': glom(self.settings, 'feishu.proxies.http') @@ -57,6 +70,20 @@ def __call__(self) -> HttpFptConfig: else None, } self.FEISHU_SEND = glom(self.settings, 'feishu.send') + + # 企业微信 + self.WECHAT_WEBHOOK = glom(self.settings, 'wechat.webhook') + self.WECHAT_PROXY = { + 'http': glom(self.settings, 'wechat.proxies.http') + if glom(self.settings, 'wechat.proxies.http') != '' + else None, + 'https': glom(self.settings, 'wechat.proxies.https') + if glom(self.settings, 'wechat.proxies.https') != '' + else None, + } + self.WECHAT_SEND = glom(self.settings, 'wechat.send') + + # 请求发送 self.REQUEST_TIMEOUT = glom(self.settings, 'request.timeout') self.REQUEST_VERIFY = glom(self.settings, 'request.verify') self.REQUEST_REDIRECTS = glom(self.settings, 'request.redirects') diff --git a/httpfpt/run.py b/httpfpt/run.py index 19426d4..2ae91ad 100644 --- a/httpfpt/run.py +++ b/httpfpt/run.py @@ -4,6 +4,7 @@ import os import shutil +import subprocess import sys from typing import Literal @@ -22,6 +23,7 @@ from httpfpt.utils.send_report.dingding import DingDing from httpfpt.utils.send_report.email import SendEmail from httpfpt.utils.send_report.feishu import FeiShu +from httpfpt.utils.send_report.wechat import WeChat from httpfpt.utils.time_control import get_current_time @@ -139,14 +141,18 @@ def startup( if httpfpt_config.FEISHU_SEND: FeiShu(test_result).send() + if httpfpt_config.WECHAT_SEND: + WeChat(test_result).send() + if allure: if not os.path.exists(httpfpt_path.allure_report_env_file): shutil.copyfile(httpfpt_path.allure_env_file, httpfpt_path.allure_report_env_file) - if allure and allure_serve: - os.popen( - f'allure generate {httpfpt_path.allure_report_dir} -o {httpfpt_path.allure_html_report_dir} ' + '--clean' - ) and os.popen(f'allure serve {httpfpt_path.allure_report_dir}') # type: ignore + if allure_serve: + subprocess.run( + f'allure generate {httpfpt_path.allure_report_dir} -o {httpfpt_path.allure_html_report_dir} --clean' + ) + subprocess.run(f'allure serve {httpfpt_path.allure_report_dir}') def run( @@ -157,7 +163,7 @@ def run( clean_cache: bool = False, pydantic_verify: bool = True, # log level - log_level: Literal['-q', '-s', '-v', '-vv'] = '-v', + log_level: Literal['-q', '-s', '-v', '-vv'] = '-s', # case path case_path: str | None = None, # html report @@ -182,7 +188,7 @@ def run( :param clean_cache: 清理 redis 缓存数据,对于脏数据,这很有用,默认关闭 :param pydantic_verify: 用例数据完整架构 pydantic 快速检测, 默认开启 :param args: pytest 运行参数 - :param log_level: 控制台打印输出级别, 默认"-v" + :param log_level: 控制台打印输出级别, 默认"-s" :param case_path: 指定当前项目下的测试用例函数, 默认为空,如果指定,则执行指定用例,否则执行全部 :param html_report: 生成 HTML 测试报告, 默认开启 :param allure: 生成 allure 测试报告, 默认开启 From beeea0342c3c4fcead83eba3818e71e8d1eb8c7e Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 11 Mar 2024 16:05:03 +0800 Subject: [PATCH 41/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/utils/send_report/wechat.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/httpfpt/utils/send_report/wechat.py b/httpfpt/utils/send_report/wechat.py index 50af58a..825251e 100644 --- a/httpfpt/utils/send_report/wechat.py +++ b/httpfpt/utils/send_report/wechat.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- - from httpfpt.common.log import log -from httpfpt.core.get_conf import config +from httpfpt.core.get_conf import httpfpt_config class WeChat: @@ -18,8 +17,8 @@ def send(self) -> None: data = { 'msgtype': 'markdown', 'markdown': { - 'content': f"# {config.TEST_REPORT_TITLE}\n" - f"> 👤 测试人员: **{config.TESTER_NAME}**\n" + 'content': f"# {httpfpt_config.TEST_REPORT_TITLE}\n" + f"> 👤 测试人员: **{httpfpt_config.TESTER_NAME}**\n" f"> 🤖 测试结果: **{self.content['result']}**\n" f"> ✅ 通过用例: **{self.content['passed']}**\n" f"> 🔧 失败用例: **{self.content['failed']}**\n" @@ -31,10 +30,10 @@ def send(self) -> None: }, } response = requests.session().post( - url=config.WECHAT_WEBHOOK, + url=httpfpt_config.WECHAT_WEBHOOK, json=data, headers=headers, - proxies=config.WECHAT_PROXY, # type: ignore + proxies=httpfpt_config.WECHAT_PROXY, # type: ignore ) response.raise_for_status() except Exception as e: From f7920bce8202e39ed60cf061b831316991931b4c Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 11 Mar 2024 17:33:56 +0800 Subject: [PATCH 42/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=BB=88=E7=AB=AF=E6=89=93=E5=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/utils/cli/new_project.py | 92 +++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index e80aa69..20d518d 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -3,13 +3,16 @@ from __future__ import annotations import os +import platform import shutil from importlib.resources import path as import_path +from time import sleep import cappa from rich.prompt import Prompt +from rich.syntax import Syntax from httpfpt.utils.rich_console import console @@ -17,46 +20,75 @@ def create_new_project() -> None: name = Prompt.ask('❓ Set your project a name', default='httpfpt_project') path = Prompt.ask('❓ Set your project path (relative or absolute path, which automatically parses.)', default='.') - console.print('\n⏳ The project is being created automatically...') if path != '.': if not os.path.isdir(path): raise cappa.Exit(f'\n❌ The "{path}" is not a directory', code=1) - # TODO: 添加 rich 创建过程进度打印 - project_path = os.path.abspath(os.sep.join([path, name])) - core_path = os.path.join(project_path, 'core') - data_path = os.path.join(project_path, 'data') - init_file = os.path.join(project_path, '__init__.py') - conftest_file = os.path.join(project_path, 'conftest.py') - pytest_file = os.path.join(project_path, 'pytest.ini') - if os.path.exists(project_path): - raise cappa.Exit(f'\n❌ The "{name}" directory is not empty', code=1) - os.makedirs(project_path) - with import_path('httpfpt.core', '') as core_data: - patterns = ['__init__.py', 'get_conf.py', 'path_conf.py'] - shutil.copytree(core_data, core_path, ignore=shutil.ignore_patterns(*patterns)) - with import_path('httpfpt.data', '') as case_data: - shutil.copytree(case_data, data_path) - with import_path('httpfpt', '__init__.py') as pytest_init: - shutil.copyfile(pytest_init, init_file) - with import_path('httpfpt', 'conftest.py') as conftest: - shutil.copyfile(conftest, conftest_file) - with import_path('httpfpt', 'pytest.ini') as pytest_ini: - shutil.copyfile(pytest_ini, pytest_file) - run_tpl = """#!/usr/bin/env python3 + with console.status('[bold green]The project is being created...[/]'): + console.print(end='\n') + sleep(2) + + project_path = os.path.abspath(os.sep.join([path, name])) + if os.path.exists(project_path): + raise cappa.Exit(f'\n❌ The "{project_path}" directory is not empty', code=1) + os.makedirs(project_path) + console.print('📁 Created the project folder') + sleep(2) + + core_path = os.path.join(project_path, 'core') + with import_path('httpfpt.core', '') as core_data: + patterns = ['__init__.py', 'get_conf.py', 'path_conf.py'] + shutil.copytree(core_data, core_path, ignore=shutil.ignore_patterns(*patterns)) + console.print('📄 Created core files') + sleep(2) + + data_path = os.path.join(project_path, 'data') + with import_path('httpfpt.data', '') as case_data: + shutil.copytree(case_data, data_path) + console.print('📄 Created case data samples') + sleep(2) + + init_file = os.path.join(project_path, '__init__.py') + with import_path('httpfpt', '__init__.py') as pytest_init: + shutil.copyfile(pytest_init, init_file) + + conftest_file = os.path.join(project_path, 'conftest.py') + with import_path('httpfpt', 'conftest.py') as conftest: + shutil.copyfile(conftest, conftest_file) + console.print('📄 Created pytest conftest') + sleep(1) + + pytest_file = os.path.join(project_path, 'pytest.ini') + with import_path('httpfpt', 'pytest.ini') as pytest_ini: + shutil.copyfile(pytest_ini, pytest_file) + console.print('📄 Created pytest ini') + sleep(1) + + run_tpl = """#!/usr/bin/env python3 # -*- coding: utf-8 -*- from httpfpt.run import run as httpfpt_run httpfpt_run(testcase_generate=True) """ - with open(os.path.join(project_path, 'run.py'), 'w', encoding='utf-8') as f: - f.write(run_tpl) + with open(os.path.join(project_path, 'run.py'), 'w', encoding='utf-8') as f: + f.write(run_tpl) + console.print('📄 Created run pytest file') + console.print( f'\n🎉 The project "{name}" has been created.' - f'\n🌴 The project is located in the directory: [cyan]{project_path}[/]' + f'\n🌳 The project is located in the directory: [cyan]{project_path}[/]' f'\n⚠️ Before accessing HTTPFPT, be sure to set the environment variable ' - f'[yellow]HTTPFPT_PROJECT_PATH[/] to the current project directory' - f'\n Windows: setx HTTPFPT_PROJECT_PATH "{project_path}"' - f'\n Unix: vim ~/.bashrc' - f'\n\t export PATH=$PATH:{project_path}' + '[yellow]HTTPFPT_PROJECT_PATH[/] to the current project directory' ) + if platform.system().lower() != 'windows': + env_var_cmd = f""" +# Windows +> setx HTTPFPT_PROJECT_PATH "{project_path}" + """ + else: + env_var_cmd = f""" +# Unix +> vim ~/.bashrc +> export PATH=$PATH:{project_path} + """ + console.print(Syntax(env_var_cmd, 'shell', line_numbers=True)) From b603e224ccecf092b4339549288c789410d0e930 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 11 Mar 2024 18:33:23 +0800 Subject: [PATCH 43/46] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=BB=88=E7=AB=AF?= =?UTF-8?q?=E6=8A=A5=E9=94=99=E5=A0=86=E6=A0=88=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/cli.py | 2 -- httpfpt/core/path_conf.py | 8 +++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/httpfpt/cli.py b/httpfpt/cli.py index 4dc6287..215b27f 100644 --- a/httpfpt/cli.py +++ b/httpfpt/cli.py @@ -10,7 +10,6 @@ import cappa from cappa import Subcommands -from rich.traceback import install as rich_install from typing_extensions import Annotated sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -177,7 +176,6 @@ def __call__(self) -> None: def cappa_invoke() -> None: """cli 执行程序""" - rich_install() cappa.invoke(HttpFptCLI) diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index 1b13799..abcfd88 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -21,7 +21,13 @@ def project_dir(self) -> str: ) else: if not os.path.exists(_base_dir): - raise ConfigInitError(f'运行失败,项目路径 {_base_dir} 不存在,请检查环境变量配置是否正确') + raise ConfigInitError(f""" + 错误:操作失败 - 未找到项目路径 '{_base_dir}'。 + 请确保以下几点: + 1. 环境变量是否已正确配置 + 2. 检查指定的项目路径是否存在。如果项目还未创建,你需要先创建一个新项目 + 完成上述检查后,请重新尝试执行此操作 + """) return _base_dir @property From 9d33f27801e6a811737216d1af4539550c81ddde Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 11 Mar 2024 18:48:22 +0800 Subject: [PATCH 44/46] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/core/path_conf.py | 12 +++++------- httpfpt/utils/cli/new_project.py | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/httpfpt/core/path_conf.py b/httpfpt/core/path_conf.py index abcfd88..0507fb5 100644 --- a/httpfpt/core/path_conf.py +++ b/httpfpt/core/path_conf.py @@ -21,13 +21,11 @@ def project_dir(self) -> str: ) else: if not os.path.exists(_base_dir): - raise ConfigInitError(f""" - 错误:操作失败 - 未找到项目路径 '{_base_dir}'。 - 请确保以下几点: - 1. 环境变量是否已正确配置 - 2. 检查指定的项目路径是否存在。如果项目还未创建,你需要先创建一个新项目 - 完成上述检查后,请重新尝试执行此操作 - """) + raise ConfigInitError( + f"操作失败 - 未找到项目路径 '{_base_dir}'。请确保以下几点:" + f'1. 环境变量是否已正确配置;2. 检查指定的项目路径是否存在。' + f'如果项目还未创建,你需要先创建一个新项目;完成上述检查后,请重新尝试执行此操作' + ) return _base_dir @property diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index 20d518d..0ea1f1f 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -80,7 +80,7 @@ def create_new_project() -> None: f'\n⚠️ Before accessing HTTPFPT, be sure to set the environment variable ' '[yellow]HTTPFPT_PROJECT_PATH[/] to the current project directory' ) - if platform.system().lower() != 'windows': + if platform.system().lower() == 'windows': env_var_cmd = f""" # Windows > setx HTTPFPT_PROJECT_PATH "{project_path}" From 8a287311603dd30785df479dbe193c995d10d8d7 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Tue, 12 Mar 2024 14:09:30 +0800 Subject: [PATCH 45/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=BB=88=E7=AB=AFshell?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E8=BE=93=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpfpt/utils/cli/new_project.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index 0ea1f1f..e89850f 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -75,10 +75,11 @@ def create_new_project() -> None: console.print('📄 Created run pytest file') console.print( - f'\n🎉 The project "{name}" has been created.' + f'\n🎉 The project <{name}> has been created.' f'\n🌳 The project is located in the directory: [cyan]{project_path}[/]' f'\n⚠️ Before accessing HTTPFPT, be sure to set the environment variable ' - '[yellow]HTTPFPT_PROJECT_PATH[/] to the current project directory' + '[yellow]HTTPFPT_PROJECT_PATH[/] to the current project directory', + end='\n\n' ) if platform.system().lower() == 'windows': env_var_cmd = f""" From 44c1eff2a29a70e98d7a8a4d9e12fb6e88dc13a4 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Tue, 12 Mar 2024 14:14:08 +0800 Subject: [PATCH 46/46] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 ++++------------ httpfpt/utils/cli/new_project.py | 2 +- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 24a531b..89e5c1d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) > [!IMPORTANT] -> 当前分支为 SDK 版本,如需修改源码进行功能定制,请切换到 [master](https://github.com/wu-clan/httpfpt) 分支,以获取更简易的本地定制化 +> 当前分支为 SDK 版本,如需修改源码进行功能定制,建议切换到 [master](https://github.com/wu-clan/httpfpt) 分支 基于 HTTP 请求的快速数据驱动 pytest 接口自动化测试框架 @@ -40,21 +40,13 @@ ## ⬇️ 下载 -克隆: - ```shell -git clone https://github.com/wu-clan/httpfpt.git +pip install httpfpt ``` ## 🧑‍💻 Use -1. 安装依赖: - - ```shell - pip install -r requirements.txt - ``` - -2. 安装 redis 数据库并启动服务 +1. 安装 redis 数据库并启动服务 [Redis Windows](https://github.com/redis-windows/redis-windows) @@ -62,7 +54,7 @@ git clone https://github.com/wu-clan/httpfpt.git [Docker](https://hub.docker.com/_/redis) -3. 安装 mysql 数据库(可选,如果你需要本地数据库) +2. 安装 mysql 数据库(可选,如果你需要本地数据库) [Windows / Linux / macOS](https://dev.mysql.com/downloads/installer/) diff --git a/httpfpt/utils/cli/new_project.py b/httpfpt/utils/cli/new_project.py index e89850f..7ac1f3a 100644 --- a/httpfpt/utils/cli/new_project.py +++ b/httpfpt/utils/cli/new_project.py @@ -79,7 +79,7 @@ def create_new_project() -> None: f'\n🌳 The project is located in the directory: [cyan]{project_path}[/]' f'\n⚠️ Before accessing HTTPFPT, be sure to set the environment variable ' '[yellow]HTTPFPT_PROJECT_PATH[/] to the current project directory', - end='\n\n' + end='\n\n', ) if platform.system().lower() == 'windows': env_var_cmd = f"""