├── python └── atomic_bomb_engine │ ├── dist │ └── .gitkeep │ ├── __init__.py │ ├── middleware.py │ ├── __init__.pyi │ └── server.py ├── .gitignore ├── img ├── img.png ├── architecture.png └── atomic-bomb-engine-logo.png ├── src ├── py_lib │ ├── mod.rs │ ├── jsonpath_extract_func.rs │ ├── think_time_option_func.rs │ ├── step_option_func.rs │ ├── assert_option_func.rs │ ├── multipart_option_func.rs │ ├── setup_option_func.rs │ ├── endpoint_func.rs │ └── batch_runner.rs ├── utils │ ├── mod.rs │ ├── depythonize.rs │ ├── create_assert_err_dict.rs │ ├── create_http_err_dict.rs │ ├── parse_assert_options.rs │ ├── create_api_results_dict.rs │ ├── parse_multipart_options.rs │ ├── parse_step_options.rs │ ├── parse_setup_options.rs │ └── parse_api_endpoints.rs └── lib.rs ├── .gitea └── workflows │ ├── build-python-wheels_on_linux.yml │ └── build-python-wheels_on_win.yml ├── Cargo.toml ├── pyproject.toml ├── LICENSE ├── .github └── workflows │ ├── build_wheels_linux.yml │ ├── build_wheels_windows.yml │ ├── build_wheels_macos.yml │ ├── build_wheels_centos8.yml │ └── build_wheels_linux(musl).yml ├── .circleci └── config.yml ├── README.md └── Cargo.lock /python/atomic_bomb_engine/dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.venv 3 | node_modules 4 | .DS_Store 5 | 6 | .idea/ 7 | -------------------------------------------------------------------------------- /img/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/we-lsp/atomic-bomb-engine-py/HEAD/img/img.png -------------------------------------------------------------------------------- /img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/we-lsp/atomic-bomb-engine-py/HEAD/img/architecture.png -------------------------------------------------------------------------------- /img/atomic-bomb-engine-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/we-lsp/atomic-bomb-engine-py/HEAD/img/atomic-bomb-engine-logo.png -------------------------------------------------------------------------------- /python/atomic_bomb_engine/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: (Apache-2.0 OR MIT) 2 | 3 | from .atomic_bomb_engine import * 4 | 5 | __doc__ = atomic_bomb_engine.__doc__ 6 | if hasattr(atomic_bomb_engine, "__all__"): 7 | __all__ = atomic_bomb_engine.__all__ 8 | 9 | -------------------------------------------------------------------------------- /src/py_lib/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod assert_option_func; 2 | pub(crate) mod batch_runner; 3 | pub(crate) mod endpoint_func; 4 | pub(crate) mod jsonpath_extract_func; 5 | pub(crate) mod multipart_option_func; 6 | pub(crate) mod setup_option_func; 7 | pub(crate) mod step_option_func; 8 | pub(crate) mod think_time_option_func; 9 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create_api_results_dict; 2 | pub mod create_assert_err_dict; 3 | pub mod create_http_err_dict; 4 | pub mod depythonize; 5 | pub mod parse_api_endpoints; 6 | pub mod parse_assert_options; 7 | pub mod parse_multipart_options; 8 | pub mod parse_setup_options; 9 | pub mod parse_step_options; 10 | -------------------------------------------------------------------------------- /python/atomic_bomb_engine/middleware.py: -------------------------------------------------------------------------------- 1 | async def cors_middleware(app, handler): 2 | async def cors_handler(request): 3 | response = await handler(request) 4 | response.headers['Access-Control-Allow-Origin'] = '*' 5 | response.headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS' 6 | response.headers['Access-Control-Allow-Headers'] = 'X-Requested-With, Content-Type' 7 | return response 8 | return cors_handler -------------------------------------------------------------------------------- /src/py_lib/jsonpath_extract_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyAny, PyDict, PyDictMethods}; 2 | use pyo3::{pyfunction, Py, PyResult, Python}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | key, 7 | jsonpath, 8 | ))] 9 | pub(crate) fn jsonpath_extract_option( 10 | py: Python, 11 | key: String, 12 | jsonpath: String, 13 | ) -> PyResult> { 14 | let dict = PyDict::new(py); 15 | dict.set_item("key", key)?; 16 | dict.set_item("jsonpath", jsonpath)?; 17 | Ok(dict.into_any().unbind()) 18 | } 19 | -------------------------------------------------------------------------------- /src/py_lib/think_time_option_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyAny, PyDict, PyDictMethods}; 2 | use pyo3::{pyfunction, Py, PyResult, Python}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | min_millis, 7 | max_millis, 8 | ))] 9 | pub(crate) fn think_time_option( 10 | py: Python, 11 | min_millis: u64, 12 | max_millis: u64, 13 | ) -> PyResult> { 14 | let dict = PyDict::new(py); 15 | dict.set_item("min_millis", min_millis)?; 16 | dict.set_item("max_millis", max_millis)?; 17 | Ok(dict.into_any().unbind()) 18 | } 19 | -------------------------------------------------------------------------------- /src/py_lib/step_option_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyAny, PyDict, PyDictMethods}; 2 | use pyo3::{pyfunction, Py, PyResult, Python}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | increase_step, 7 | increase_interval, 8 | ))] 9 | pub(crate) fn step_option( 10 | py: Python, 11 | increase_step: usize, 12 | increase_interval: usize, 13 | ) -> PyResult> { 14 | let dict = PyDict::new(py); 15 | dict.set_item("increase_step", increase_step)?; 16 | dict.set_item("increase_interval", increase_interval)?; 17 | Ok(dict.into_any().unbind()) 18 | } 19 | -------------------------------------------------------------------------------- /src/py_lib/assert_option_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyDict, PyDictMethods}; 2 | use pyo3::{pyfunction, Py, PyResult, Python}; 3 | use pyo3::types::PyAny; 4 | 5 | #[pyfunction] 6 | #[pyo3(signature=( 7 | jsonpath, 8 | reference_object, 9 | ))] 10 | pub(crate) fn assert_option( 11 | py: Python, 12 | jsonpath: String, 13 | reference_object: Py, 14 | ) -> PyResult> { 15 | let dict = PyDict::new(py); 16 | dict.set_item("jsonpath", jsonpath)?; 17 | dict.set_item("reference_object", reference_object)?; 18 | Ok(dict.into_any().unbind()) 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/depythonize.rs: -------------------------------------------------------------------------------- 1 | use pyo3::exceptions::PyRuntimeError; 2 | use pyo3::prelude::*; 3 | use pyo3::types::PyModule; 4 | use serde::de::DeserializeOwned; 5 | 6 | pub fn depythonize(value: &Bound) -> PyResult 7 | where 8 | T: DeserializeOwned, 9 | { 10 | let py = value.py(); 11 | let json_module = PyModule::import(py, "json")?; 12 | let dumps = json_module.getattr("dumps")?; 13 | let json_str: String = dumps.call1((value,))?.extract()?; 14 | serde_json::from_str(&json_str) 15 | .map_err(|err| PyErr::new::(format!("Error: {:?}", err))) 16 | } 17 | -------------------------------------------------------------------------------- /src/py_lib/multipart_option_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyAny, PyDict, PyDictMethods}; 2 | use pyo3::{pyfunction, Py, PyResult, Python}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | form_key, 7 | path, 8 | file_name, 9 | mime, 10 | ))] 11 | pub(crate) fn multipart_option( 12 | py: Python, 13 | form_key: String, 14 | path: String, 15 | file_name: String, 16 | mime: String, 17 | ) -> PyResult> { 18 | let dict = PyDict::new(py); 19 | dict.set_item("form_key", form_key)?; 20 | dict.set_item("path", path)?; 21 | dict.set_item("file_name", file_name)?; 22 | dict.set_item("mime", mime)?; 23 | Ok(dict.into_any().unbind()) 24 | } 25 | -------------------------------------------------------------------------------- /.gitea/workflows/build-python-wheels_on_linux.yml: -------------------------------------------------------------------------------- 1 | name: Build Python Wheels on Linux 2 | on: 3 | push: 4 | branches: 5 | - release 6 | jobs: 7 | build: 8 | runs-on: linux_amd64 9 | 10 | env: 11 | http_proxy: http://10.0.0.54:1080 12 | https_proxy: http://10.0.0.54:1080 13 | all_proxy: socks5://10.0.0.54:1080 14 | 15 | steps: 16 | - name: 签出代码 17 | uses: actions/checkout@v4 18 | 19 | - name: 设置cargo环境变量 20 | run: echo "PATH=$PATH:/home/qyzhg/.cargo/bin" >> $GITHUB_ENV 21 | 22 | - name: 构建python包 23 | run: /home/qyzhg/maturin-builder/bin/maturin build --release -i python3.8 -i python3.9 -i python3.10 -i python3.11 -i python3.12 24 | 25 | - name: 上传构建的whl包到PyPI 26 | run: | 27 | /home/qyzhg/maturin-builder/bin/twine upload --repository pypi --config-file /home/qyzhg/.pypirc ./target/wheels/* 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "atomic-bomb-engine-py" 3 | version = "0.42.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | name = "atomic_bomb_engine" 8 | crate-type = ["cdylib"] 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | atomic-bomb-engine = "0.41.1" 14 | #atomic-bomb-engine = { git = "https://github.com/we-lsp/atomic-bomb-engine.git", branch = "ExponentialMovingAverage"} 15 | tokio = "1" 16 | pyo3-async-runtimes = { version = "0.27", features = ["attributes", "tokio-runtime"] } 17 | serde_json = { version = "1.0", features = [] } 18 | futures = "0.3" 19 | anyhow = "1.0" 20 | serde = "1.0.202" 21 | 22 | [build] 23 | rustflags = ["-C", "target-feature=+crt-static"] 24 | 25 | 26 | [dependencies.pyo3] 27 | version = "0.27" 28 | features = ["extension-module", "auto-initialize", "serde"] 29 | 30 | [tool.maturin] 31 | name = "atomic_bomb_engine" 32 | scripts = "python/atomic_bomb_engine" 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.4"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "atomic_bomb_engine" 7 | version = "0.42.0" 8 | description = "使用rust开发的高性能python压测工具" 9 | license = "MIT" 10 | readme = "README.md" 11 | repository = "https://github.com/qyzhg/atomic-bomb-engine-py" 12 | keywords = ["rust", "python", "binding"] 13 | requires-python = ">=3.8" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.9", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | "Programming Language :: Python :: 3.13", 21 | "Programming Language :: Rust", 22 | "Operating System :: OS Independent", 23 | ] 24 | authors = [{name = "qyzhg", email = "qyzhg@qyzhg.com"}] 25 | dependencies = ["aiohttp", "aiosqlite"] 26 | 27 | 28 | [tool.maturin] 29 | name = "atomic_bomb_engine" 30 | python-source = "python" 31 | 32 | [tool.poetry.include] 33 | include = ["python/atomic_bomb_engine/dist/**/*"] 34 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | use pyo3::types::PyModule; 3 | mod py_lib; 4 | mod utils; 5 | 6 | #[pymodule] 7 | #[pyo3(name = "atomic_bomb_engine")] 8 | fn atomic_bomb_engine(m: &Bound<'_, PyModule>) -> PyResult<()> { 9 | m.add_function(wrap_pyfunction!( 10 | py_lib::assert_option_func::assert_option, 11 | m 12 | )?)?; 13 | m.add_function(wrap_pyfunction!(py_lib::endpoint_func::endpoint, m)?)?; 14 | m.add_function(wrap_pyfunction!(py_lib::step_option_func::step_option, m)?)?; 15 | m.add_function(wrap_pyfunction!( 16 | py_lib::setup_option_func::setup_option, 17 | m 18 | )?)?; 19 | m.add_function(wrap_pyfunction!( 20 | py_lib::jsonpath_extract_func::jsonpath_extract_option, 21 | m 22 | )?)?; 23 | m.add_function(wrap_pyfunction!( 24 | py_lib::think_time_option_func::think_time_option, 25 | m 26 | )?)?; 27 | m.add_function(wrap_pyfunction!( 28 | py_lib::multipart_option_func::multipart_option, 29 | m 30 | )?)?; 31 | m.add_class::()?; 32 | Ok(()) 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 qyzhg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitea/workflows/build-python-wheels_on_win.yml: -------------------------------------------------------------------------------- 1 | name: Build Python Wheels on Windows 2 | on: 3 | push: 4 | branches: 5 | - release 6 | jobs: 7 | build: 8 | runs-on: win_amd64 9 | # Start-Process -FilePath "C:\Users\Administrator\act_runner.exe" -ArgumentList "daemon -c C:\Users\Administrator\.runner" 10 | 11 | env: 12 | http_proxy: http://10.0.0.54:1080 13 | https_proxy: http://10.0.0.54:1080 14 | all_proxy: socks5://10.0.0.54:1080 15 | 16 | steps: 17 | - name: 签出代码 18 | uses: actions/checkout@v3 19 | - name: 构建python包 20 | run: | 21 | cargo --version 22 | C:\Users\Administrator\rust-build\Scripts\maturin.exe build --release --interpreter python3.8 --interpreter python3.9 --interpreter python3.10 --interpreter python3.11 --interpreter python3.12 23 | env: 24 | PYTHONIOENCODING: utf-8 25 | - name: 上传构建的whl包到PyPI 26 | run: | 27 | C:\Users\Administrator\rust-build\Scripts\twine.exe upload --repository pypi --config-file C:\Users\Administrator\.pypirc .\target\wheels\* 28 | env: 29 | PYTHONIOENCODING: utf-8 30 | -------------------------------------------------------------------------------- /src/utils/create_assert_err_dict.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use atomic_bomb_engine::models::assert_error_stats::AssertErrKey; 4 | use pyo3::types::{PyDict, PyDictMethods, PyList}; 5 | use pyo3::{Py, PyResult, Python}; 6 | 7 | pub fn create_assert_error_dict( 8 | py: Python<'_>, 9 | assert_errors: &HashMap, 10 | ) -> PyResult> { 11 | if assert_errors.is_empty() { 12 | return Ok(PyList::empty(py).unbind()); 13 | } 14 | 15 | let mut result_errors: Vec> = Vec::with_capacity(assert_errors.len()); 16 | for (assert_error_key, count) in assert_errors { 17 | let assert_error_dict = PyDict::new(py); 18 | assert_error_dict.set_item("name", &assert_error_key.name)?; 19 | assert_error_dict.set_item("message", &assert_error_key.msg)?; 20 | assert_error_dict.set_item("url", &assert_error_key.url)?; 21 | assert_error_dict.set_item("host", &assert_error_key.host)?; 22 | assert_error_dict.set_item("path", &assert_error_key.path)?; 23 | assert_error_dict.set_item("count", count)?; 24 | result_errors.push(assert_error_dict.unbind()); 25 | } 26 | 27 | Ok(PyList::new(py, result_errors)?.unbind()) 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/create_http_err_dict.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use atomic_bomb_engine::models::http_error_stats::HttpErrKey; 4 | use pyo3::types::{PyDict, PyDictMethods, PyList}; 5 | use pyo3::{Py, PyResult, Python}; 6 | 7 | pub fn create_http_error_dict( 8 | py: Python<'_>, 9 | http_errors: &HashMap, 10 | ) -> PyResult> { 11 | if http_errors.is_empty() { 12 | return Ok(PyList::empty(py).unbind()); 13 | } 14 | 15 | let mut http_errors_list: Vec> = Vec::with_capacity(http_errors.len()); 16 | for (http_error_key, count) in http_errors { 17 | let http_error_dict = PyDict::new(py); 18 | http_error_dict.set_item("name", &http_error_key.name)?; 19 | http_error_dict.set_item("code", http_error_key.code)?; 20 | http_error_dict.set_item("message", &http_error_key.msg)?; 21 | http_error_dict.set_item("url", &http_error_key.url)?; 22 | http_error_dict.set_item("host", &http_error_key.host)?; 23 | http_error_dict.set_item("path", &http_error_key.path)?; 24 | http_error_dict.set_item("source", &http_error_key.source)?; 25 | http_error_dict.set_item("count", count)?; 26 | http_errors_list.push(http_error_dict.unbind()); 27 | } 28 | 29 | Ok(PyList::new(py, http_errors_list)?.unbind()) 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/parse_assert_options.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::depythonize::depythonize; 2 | use atomic_bomb_engine::models; 3 | use pyo3::exceptions::PyRuntimeError; 4 | use pyo3::prelude::*; 5 | use pyo3::types::{PyDict, PyList, PyListMethods}; 6 | use serde_json::Value; 7 | 8 | pub fn _new( 9 | py: Python<'_>, 10 | assert_options: Option>, 11 | ) -> PyResult>> { 12 | match assert_options { 13 | None => Ok(None), 14 | Some(ops_list) => { 15 | let list = ops_list.bind(py); 16 | let mut ops: Vec = Vec::with_capacity(list.len()); 17 | for item in list.iter() { 18 | let dict = item.cast::()?; 19 | let jsonpath: String = dict 20 | .get_item("jsonpath")? 21 | .ok_or_else(|| { 22 | PyErr::new::("必须输入一个jsonpath".to_string()) 23 | })? 24 | .extract()?; 25 | 26 | let reference_any = dict.get_item("reference_object")?.ok_or_else(|| { 27 | PyErr::new::("必须输入一个reference_object".to_string()) 28 | })?; 29 | let reference_object: Value = depythonize(&reference_any)?; 30 | 31 | ops.push(models::assert_option::AssertOption { 32 | jsonpath, 33 | reference_object, 34 | }); 35 | } 36 | Ok(Some(ops)) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/py_lib/setup_option_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyAny, PyDict, PyDictMethods}; 2 | use pyo3::{pyfunction, Py, PyResult, Python}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | name, 7 | url, 8 | method, 9 | json=None, 10 | form_data=None, 11 | multipart_options=None, 12 | headers=None, 13 | cookies=None, 14 | jsonpath_extract=None, 15 | ))] 16 | pub(crate) fn setup_option( 17 | py: Python, 18 | name: String, 19 | url: String, 20 | method: String, 21 | json: Option>, 22 | form_data: Option>, 23 | multipart_options: Option>, 24 | headers: Option>, 25 | cookies: Option, 26 | jsonpath_extract: Option>, 27 | ) -> PyResult> { 28 | let dict = PyDict::new(py); 29 | dict.set_item("name", name)?; 30 | dict.set_item("url", url)?; 31 | dict.set_item("method", method)?; 32 | if let Some(json) = json { 33 | dict.set_item("json", json)?; 34 | }; 35 | if let Some(form_data) = form_data { 36 | dict.set_item("form_data", form_data)?; 37 | }; 38 | if let Some(multipart_options) = multipart_options { 39 | dict.set_item("multipart_options", multipart_options)?; 40 | }; 41 | if let Some(headers) = headers { 42 | dict.set_item("headers", headers)?; 43 | }; 44 | if let Some(cookies) = cookies { 45 | dict.set_item("cookies", cookies)?; 46 | }; 47 | if let Some(jsonpath_extract) = jsonpath_extract { 48 | dict.set_item("jsonpath_extract", jsonpath_extract)?; 49 | }; 50 | Ok(dict.into_any().unbind()) 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/create_api_results_dict.rs: -------------------------------------------------------------------------------- 1 | use atomic_bomb_engine::models::result::ApiResult; 2 | use pyo3::types::{PyDict, PyDictMethods, PyList}; 3 | use pyo3::{Py, PyResult, Python}; 4 | 5 | pub fn create_api_results_dict(py: Python<'_>, api_results: Vec) -> PyResult> { 6 | if api_results.is_empty() { 7 | return Ok(PyList::empty(py).unbind()); 8 | } 9 | 10 | let mut results: Vec> = Vec::with_capacity(api_results.len()); 11 | 12 | for result in api_results { 13 | let res_dict = PyDict::new(py); 14 | 15 | res_dict.set_item("name", result.name)?; 16 | res_dict.set_item("url", result.url)?; 17 | res_dict.set_item("host", result.host)?; 18 | res_dict.set_item("path", result.path)?; 19 | res_dict.set_item("method", result.method)?; 20 | res_dict.set_item("success_rate", result.success_rate)?; 21 | res_dict.set_item("error_rate", result.error_rate)?; 22 | res_dict.set_item("median_response_time", result.median_response_time)?; 23 | res_dict.set_item("response_time_95", result.response_time_95)?; 24 | res_dict.set_item("response_time_99", result.response_time_99)?; 25 | res_dict.set_item("total_requests", result.total_requests)?; 26 | res_dict.set_item("rps", result.rps)?; 27 | res_dict.set_item("max_response_time", result.max_response_time)?; 28 | res_dict.set_item("min_response_time", result.min_response_time)?; 29 | res_dict.set_item("err_count", result.err_count)?; 30 | res_dict.set_item("total_data_kb", result.total_data_kb)?; 31 | res_dict.set_item("throughput_per_second_kb", result.throughput_per_second_kb)?; 32 | res_dict.set_item("concurrent_number", result.concurrent_number)?; 33 | 34 | results.push(res_dict.unbind()); 35 | } 36 | 37 | Ok(PyList::new(py, results)?.unbind()) 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_linux.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python wheels for Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | - release-rc.* 8 | 9 | jobs: 10 | build-wheels: 11 | name: Build wheels on Ubuntu 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 16 | 17 | steps: 18 | - name: 签出rust代码 19 | uses: actions/checkout@v2 20 | 21 | - name: 签出前端 22 | uses: actions/checkout@v2 23 | with: 24 | repository: GiantAxeWhy/atomic-bomb-engine-front 25 | path: vue-project 26 | 27 | - name: 安装Node.js 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: '18' 31 | 32 | - name: 构建前端 33 | run: | 34 | cd vue-project 35 | npm install 36 | npm run build 37 | 38 | - name: 删除原有dist 39 | run: rm -rf python/atomic_bomb_engine/dist/* 40 | 41 | - name: 复制dist到Python项目 42 | run: cp -r vue-project/dist/* python/atomic_bomb_engine/dist/ 43 | 44 | - name: 设置python环境 45 | uses: actions/setup-python@v2 46 | with: 47 | python-version: ${{ matrix.python-version }} 48 | 49 | - name: 安装Rust 50 | uses: actions-rs/toolchain@v1 51 | with: 52 | profile: minimal 53 | toolchain: stable 54 | override: true 55 | 56 | - name: 安装maturin 57 | run: pip install maturin 58 | 59 | - name: 构建wheels 60 | run: maturin build --release --interpreter python${{ matrix.python-version }} 61 | 62 | - name: 安装Twine 63 | run: pip install twine 64 | 65 | - name: 上传wheels到PyPI 66 | env: 67 | TWINE_USERNAME: __token__ 68 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 69 | run: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ target/wheels/*.whl 70 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_windows.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python wheels for Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | - release-rc.* 8 | 9 | jobs: 10 | build-wheels: 11 | name: Build wheels on Windows 12 | runs-on: windows-latest 13 | strategy: 14 | matrix: 15 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 16 | 17 | steps: 18 | - name: 签出rust代码 19 | uses: actions/checkout@v2 20 | 21 | - name: 签出前端 22 | uses: actions/checkout@v2 23 | with: 24 | repository: GiantAxeWhy/atomic-bomb-engine-front 25 | path: vue-project 26 | 27 | - name: 安装Node.js 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: '18' 31 | 32 | - name: 构建前端 33 | run: | 34 | cd vue-project 35 | npm install 36 | npm run build 37 | 38 | - name: 清空目标路径 39 | run: Remove-Item -Path python\atomic_bomb_engine\dist\* -Recurse -Force 40 | 41 | - name: 复制dist到Python项目 42 | run: Copy-Item vue-project/dist/* python\atomic_bomb_engine\dist\ -Recurse 43 | 44 | - name: 设置python环境 45 | uses: actions/setup-python@v2 46 | with: 47 | python-version: ${{ matrix.python-version }} 48 | 49 | - name: 安装Rust 50 | uses: actions-rs/toolchain@v1 51 | with: 52 | profile: minimal 53 | toolchain: stable 54 | override: true 55 | 56 | - name: 安装maturin 57 | run: pip install maturin 58 | 59 | - name: 构建wheels 60 | run: maturin build --release --interpreter python${{ matrix.python-version }} 61 | 62 | - name: 安装Twine 63 | run: pip install twine 64 | 65 | - name: 上传wheels到PyPI 66 | env: 67 | TWINE_USERNAME: __token__ 68 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 69 | run: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ target/wheels/*.whl 70 | -------------------------------------------------------------------------------- /src/py_lib/endpoint_func.rs: -------------------------------------------------------------------------------- 1 | use pyo3::types::{PyAny, PyDict, PyDictMethods}; 2 | use pyo3::{pyfunction, Py, PyResult, Python}; 3 | 4 | #[pyfunction] 5 | #[pyo3(signature=( 6 | name, 7 | url, 8 | method, 9 | weight, 10 | json=None, 11 | form_data=None, 12 | multipart_options=None, 13 | headers=None, 14 | cookies=None, 15 | assert_options=None, 16 | think_time_option=None, 17 | setup_options=None, 18 | ))] 19 | pub(crate) fn endpoint( 20 | py: Python, 21 | name: String, 22 | url: String, 23 | method: String, 24 | weight: u32, 25 | json: Option>, 26 | form_data: Option>, 27 | multipart_options: Option>, 28 | headers: Option>, 29 | cookies: Option, 30 | assert_options: Option>, 31 | think_time_option: Option>, 32 | setup_options: Option>, 33 | ) -> PyResult> { 34 | let dict = PyDict::new(py); 35 | dict.set_item("name", name)?; 36 | dict.set_item("url", url)?; 37 | dict.set_item("method", method)?; 38 | dict.set_item("weight", weight)?; 39 | if let Some(json) = json { 40 | dict.set_item("json", json)?; 41 | }; 42 | if let Some(form_data) = form_data { 43 | dict.set_item("form_data", form_data)?; 44 | }; 45 | if let Some(multipart_options) = multipart_options { 46 | dict.set_item("multipart_options", multipart_options)?; 47 | }; 48 | if let Some(headers) = headers { 49 | dict.set_item("headers", headers)?; 50 | }; 51 | if let Some(cookies) = cookies { 52 | dict.set_item("cookies", cookies)?; 53 | }; 54 | if let Some(assert_options) = assert_options { 55 | dict.set_item("assert_options", assert_options)?; 56 | }; 57 | if let Some(think_time_option) = think_time_option { 58 | dict.set_item("think_time_option", think_time_option)?; 59 | }; 60 | if let Some(setup_options) = setup_options { 61 | dict.set_item("setup_options", setup_options)?; 62 | } 63 | Ok(dict.into_any().unbind()) 64 | } 65 | -------------------------------------------------------------------------------- /src/utils/parse_multipart_options.rs: -------------------------------------------------------------------------------- 1 | use atomic_bomb_engine::models; 2 | use pyo3::exceptions::PyRuntimeError; 3 | use pyo3::prelude::*; 4 | use pyo3::types::{PyDict, PyList, PyListMethods}; 5 | 6 | pub fn new( 7 | py: Python<'_>, 8 | py_multipart_options: Option>, 9 | ) -> PyResult>> { 10 | match py_multipart_options { 11 | None => Ok(None), 12 | Some(ops_list) => { 13 | let list = ops_list.bind(py); 14 | let mut multipart_options: Vec = 15 | Vec::with_capacity(list.len()); 16 | 17 | for item in list.iter() { 18 | let dict = item.cast::()?; 19 | 20 | let form_key: String = dict 21 | .get_item("form_key")? 22 | .ok_or_else(|| { 23 | PyErr::new::("form_key 不能为空".to_string()) 24 | })? 25 | .extract()?; 26 | 27 | let path: String = dict 28 | .get_item("path")? 29 | .ok_or_else(|| PyErr::new::("path 不能为空".to_string()))? 30 | .extract()?; 31 | 32 | let file_name: String = dict 33 | .get_item("file_name")? 34 | .ok_or_else(|| { 35 | PyErr::new::("file_name 不能为空".to_string()) 36 | })? 37 | .extract()?; 38 | 39 | let mime: String = dict 40 | .get_item("mime")? 41 | .ok_or_else(|| PyErr::new::("mime 不能为空".to_string()))? 42 | .extract()?; 43 | 44 | multipart_options.push(models::multipart_option::MultipartOption { 45 | form_key, 46 | path, 47 | file_name, 48 | mime, 49 | }); 50 | } 51 | 52 | Ok(Some(multipart_options)) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_macos.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python wheels for macOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | - release-rc.* 8 | 9 | jobs: 10 | build-wheels-macos: 11 | runs-on: macos-latest 12 | strategy: 13 | matrix: 14 | arch: ['x86_64-apple-darwin', 'aarch64-apple-darwin'] 15 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 16 | 17 | steps: 18 | - name: 签出rust代码 19 | uses: actions/checkout@v2 20 | 21 | - name: 签出前端 22 | uses: actions/checkout@v2 23 | with: 24 | repository: GiantAxeWhy/atomic-bomb-engine-front 25 | path: vue-project 26 | 27 | - name: 安装Node.js 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: '18' 31 | 32 | - name: 构建前端 33 | run: | 34 | cd vue-project 35 | npm install 36 | npm run build 37 | 38 | - name: 删除原有dist 39 | run: rm -rf python/atomic_bomb_engine/dist/* 40 | 41 | - name: 复制dist到Python项目 42 | run: cp -r vue-project/dist/* python/atomic_bomb_engine/dist/ 43 | 44 | - name: 设置python环境 45 | uses: actions/setup-python@v5 46 | with: 47 | python-version: ${{ matrix.python-version }} 48 | 49 | - name: 安装Rust 50 | uses: actions-rs/toolchain@v1 51 | with: 52 | profile: minimal 53 | toolchain: stable 54 | override: true 55 | 56 | - name: 为目标架构添加Rust目标 57 | run: | 58 | rustup target add x86_64-apple-darwin 59 | rustup target add aarch64-apple-darwin 60 | 61 | - name: 安装maturin 62 | run: pip install maturin 63 | 64 | - name: 构建wheels 65 | run: maturin build --release --target ${{ matrix.arch }} -i python${{ matrix.python-version }} 66 | 67 | - name: 安装Twine 68 | run: pip install twine 69 | 70 | - name: 上传wheels到PyPI 71 | env: 72 | TWINE_USERNAME: __token__ 73 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 74 | run: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ target/wheels/*.whl 75 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | workflows: 4 | version: 2 5 | build_and_deploy: 6 | jobs: 7 | - build-wheels: 8 | filters: 9 | branches: 10 | only: 11 | - release 12 | 13 | jobs: 14 | build-wheels: 15 | macos: 16 | xcode: "11.3.0" 17 | steps: 18 | - checkout 19 | 20 | - run: 21 | name: Checkout frontend 22 | command: | 23 | git clone https://github.com/GiantAxeWhy/atomic-bomb-engine-front vue-project 24 | 25 | - run: 26 | name: Start Alpine container 27 | command: | 28 | docker run --name python-alpine-container -d -v "${PWD}:/__w" -w "/__w" alpine:latest sleep 3600 29 | 30 | - run: 31 | name: Install Node.js and build tools 32 | command: | 33 | docker exec python-alpine-container apk add --no-cache nodejs npm build-base 34 | 35 | - run: 36 | name: Build frontend 37 | command: | 38 | docker exec python-alpine-container sh -c "cd vue-project && npm install && npm run build" 39 | 40 | - run: 41 | name: Install pyenv and specific Python versions 42 | command: | 43 | docker exec python-alpine-container apk add --no-cache bash git curl openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev 44 | docker exec python-alpine-container sh -c "curl https://pyenv.run | bash" 45 | docker exec python-alpine-container sh -c 'export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" && for version in $(echo 3.8 3.9 3.10 3.11 3.12); do pyenv install $version; pyenv global $version; curl https://bootstrap.pypa.io/get-pip.py | python; done' 46 | 47 | - run: 48 | name: Install Rust, maturin, and patchelf 49 | command: | 50 | docker exec python-alpine-container apk add --no-cache rust cargo patchelf 51 | docker exec python-alpine-container sh -c 'export PATH="/root/.pyenv/shims:$PATH" && pip install maturin' 52 | 53 | - run: 54 | name: Build wheels using maturin 55 | command: | 56 | docker exec python-alpine-container sh -c ' 57 | export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" 58 | for version in $(echo 3.8 3.9 3.10 3.11 3.12); do 59 | pyenv global $version 60 | maturin build --release 61 | done' 62 | 63 | - run: 64 | name: Copy wheels to Runner 65 | command: docker cp python-alpine-container:/__w/target/wheels ./ 66 | 67 | - run: 68 | name: Install Twine 69 | command: pip install twine 70 | 71 | - run: 72 | name: Upload wheels to PyPI 73 | command: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ wheels/*.whl 74 | environment: 75 | TWINE_USERNAME: __token__ 76 | TWINE_PASSWORD: $PYPI_PASSWORD 77 | -------------------------------------------------------------------------------- /src/utils/parse_step_options.rs: -------------------------------------------------------------------------------- 1 | use atomic_bomb_engine::models; 2 | use pyo3::prelude::*; 3 | use pyo3::types::PyDict; 4 | 5 | pub fn new(py: Python, step_option: Option>) -> PyResult> { 6 | match step_option { 7 | None => Ok(None), 8 | Some(ops_dict) => { 9 | let dict_ref = ops_dict.bind(py); 10 | 11 | let increase_step: usize = match dict_ref.get_item("increase_step") { 12 | Ok(increase_step_py_any) => match increase_step_py_any { 13 | None => { 14 | return Err(PyErr::new::( 15 | "必须输入一个increase_step".to_string(), 16 | )) 17 | } 18 | Some(increase_step) => { 19 | let result: PyResult = increase_step.extract::(); 20 | match result { 21 | Ok(res) => res, 22 | Err(e) => { 23 | return Err(PyErr::new::( 24 | format!("increase_step必须是一个整数::{:?}", e), 25 | )) 26 | } 27 | } 28 | } 29 | }, 30 | Err(e) => { 31 | return Err(PyErr::new::(format!( 32 | "Error: {:?}", 33 | e 34 | ))) 35 | } 36 | }; 37 | 38 | let increase_interval: u64 = match dict_ref.get_item("increase_interval") { 39 | Ok(increase_interval_py_any) => match increase_interval_py_any { 40 | None => { 41 | return Err(PyErr::new::( 42 | "必须输入一个increase_interval".to_string(), 43 | )) 44 | } 45 | Some(increase_interval) => { 46 | let result: PyResult = increase_interval.extract::(); 47 | match result { 48 | Ok(res) => res, 49 | Err(e) => { 50 | return Err(PyErr::new::( 51 | format!("increase_interval必须是一个整数::{:?}", e), 52 | )) 53 | } 54 | } 55 | } 56 | }, 57 | Err(e) => { 58 | return Err(PyErr::new::(format!( 59 | "Error: {:?}", 60 | e 61 | ))) 62 | } 63 | }; 64 | 65 | let step_opt = models::step_option::StepOption { 66 | increase_step, 67 | increase_interval, 68 | }; 69 | Ok(Some(step_opt)) 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/utils/parse_setup_options.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::utils::depythonize::depythonize; 4 | use atomic_bomb_engine::models; 5 | use pyo3::exceptions::PyRuntimeError; 6 | use pyo3::prelude::*; 7 | use pyo3::types::{PyDict, PyList, PyListMethods}; 8 | use serde_json::Value; 9 | 10 | pub fn new( 11 | py: Python<'_>, 12 | py_setup_options: Option>, 13 | ) -> PyResult>> { 14 | match py_setup_options { 15 | None => Ok(None), 16 | Some(options) => { 17 | let list = options.bind(py); 18 | let mut setup_options: Vec = 19 | Vec::with_capacity(list.len()); 20 | 21 | for item in list.iter() { 22 | let dict = item.cast::()?; 23 | 24 | let name: String = dict 25 | .get_item("name")? 26 | .ok_or_else(|| PyErr::new::("name不能为空".to_string()))? 27 | .extract()?; 28 | 29 | let url: String = dict 30 | .get_item("url")? 31 | .ok_or_else(|| PyErr::new::("url不能为空".to_string()))? 32 | .extract()?; 33 | 34 | let method: String = dict 35 | .get_item("method")? 36 | .ok_or_else(|| PyErr::new::("method不能为空".to_string()))? 37 | .extract()?; 38 | 39 | let json: Option = dict 40 | .get_item("json")? 41 | .map(|value| depythonize(&value)) 42 | .transpose()?; 43 | 44 | let form_data: Option> = dict 45 | .get_item("form_data")? 46 | .map(|value| depythonize(&value)) 47 | .transpose()?; 48 | 49 | let headers: Option> = dict 50 | .get_item("headers")? 51 | .map(|value| depythonize(&value)) 52 | .transpose()?; 53 | 54 | let cookies: Option = dict 55 | .get_item("cookies")? 56 | .map(|value| depythonize(&value)) 57 | .transpose()?; 58 | 59 | let jsonpath_extract: Option> = dict 60 | .get_item("jsonpath_extract")? 61 | .map(|value| depythonize(&value)) 62 | .transpose()?; 63 | 64 | let multipart_options: Option> = 65 | dict.get_item("multipart_options")? 66 | .map(|value| depythonize(&value)) 67 | .transpose()?; 68 | 69 | setup_options.push(models::setup::SetupApiEndpoint { 70 | name, 71 | url, 72 | method, 73 | json, 74 | form_data, 75 | multipart_options, 76 | headers, 77 | cookies, 78 | jsonpath_extract, 79 | }); 80 | } 81 | 82 | Ok(Some(setup_options)) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_centos8.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python wheels for CentOS 8 2 | 3 | on: 4 | push: 5 | branches: 6 | - build-centos8 7 | 8 | jobs: 9 | build-wheels: 10 | name: Build wheels on CentOS 8 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 15 | 16 | steps: 17 | - name: 签出代码 18 | uses: actions/checkout@v2 19 | 20 | - name: 签出前端 21 | uses: actions/checkout@v2 22 | with: 23 | repository: GiantAxeWhy/atomic-bomb-engine-front 24 | path: vue-project 25 | 26 | - name: 启动 Node.js 容器 27 | run: | 28 | docker run --name node-container -d -v "${{ github.workspace }}:/workspace" -w "/workspace/vue-project" node:18 sleep 3600 29 | 30 | - name: 构建前端 31 | run: | 32 | docker exec node-container npm install 33 | docker exec node-container npm run build 34 | 35 | - name: 停止并删除 Node.js 容器 36 | run: | 37 | docker stop node-container 38 | docker rm node-container 39 | 40 | - name: 启动 AlmaLinux 容器 41 | run: | 42 | docker run --name python-almalinux-container -d -v "${{ github.workspace }}:/workspace" -w "/workspace" almalinux:8 sleep 3600 43 | 44 | - name: 安装构建工具 45 | run: | 46 | docker exec python-almalinux-container yum install -y epel-release 47 | docker exec python-almalinux-container yum install -y gcc gcc-c++ make 48 | 49 | - name: 安装 pyenv 和指定版本的 Python 50 | run: | 51 | docker exec python-almalinux-container yum install -y git curl openssl-devel bzip2-devel zlib-devel readline-devel sqlite-devel 52 | docker exec python-almalinux-container sh -c "curl https://pyenv.run | bash" 53 | docker exec python-almalinux-container sh -c 'export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" && for version in ${{ join(matrix.python-version, ' ') }}; do pyenv install $version; pyenv global $version; curl https://bootstrap.pypa.io/get-pip.py | python; done' 54 | 55 | - name: 安装 Rust、maturin 和 patchelf 56 | run: | 57 | docker exec python-almalinux-container yum remove -y patchelf 58 | docker exec python-almalinux-container yum install -y epel-release 59 | docker exec python-almalinux-container yum install -y https://download-ib01.fedoraproject.org/pub/epel/8/Everything/x86_64/Packages/p/patchelf-0.12-1.el8.x86_64.rpm 60 | docker exec python-almalinux-container patchelf --version 61 | docker exec python-almalinux-container yum install -y rust cargo 62 | docker exec python-almalinux-container sh -c 'export PATH="/root/.pyenv/shims:$PATH" && pip install maturin' 63 | 64 | - name: 使用 maturin 构建 wheels 65 | run: | 66 | docker exec python-almalinux-container sh -c ' 67 | export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" 68 | for version in ${{ join(matrix.python-version, ' ') }}; do 69 | pyenv global $version 70 | maturin build --release --skip-auditwheel 71 | done' 72 | 73 | - name: 复制 wheels 到 Runner 74 | run: docker cp python-almalinux-container:/workspace/target/wheels ./ 75 | 76 | - name: 安装 Twine 77 | run: pip install twine 78 | 79 | - name: 上传 wheels 到 PyPI 80 | env: 81 | TWINE_USERNAME: __token__ 82 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 83 | run: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ wheels/*.whl 84 | -------------------------------------------------------------------------------- /src/utils/parse_api_endpoints.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | use crate::utils::depythonize::depythonize; 3 | use std::collections::HashMap; 4 | 5 | use atomic_bomb_engine::models; 6 | use atomic_bomb_engine::models::api_endpoint::ThinkTime; 7 | use atomic_bomb_engine::models::assert_option::AssertOption; 8 | use pyo3::exceptions::PyRuntimeError; 9 | use pyo3::prelude::*; 10 | use pyo3::types::{PyDict, PyList, PyListMethods}; 11 | use serde_json::Value; 12 | 13 | pub fn new( 14 | py: Python<'_>, 15 | api_endpoints: Py, 16 | ) -> PyResult> { 17 | let mut endpoints: Vec = Vec::new(); 18 | let bound_list = api_endpoints.bind(py); 19 | for item in bound_list.iter() { 20 | let dict = item.cast::()?; 21 | 22 | let name: String = dict 23 | .get_item("name")? 24 | .ok_or_else(|| PyErr::new::("name不能为空".to_string()))? 25 | .extract()?; 26 | 27 | let url: String = dict 28 | .get_item("url")? 29 | .ok_or_else(|| PyErr::new::("url不能为空".to_string()))? 30 | .extract()?; 31 | 32 | let method: String = dict 33 | .get_item("method")? 34 | .ok_or_else(|| PyErr::new::("method不能为空".to_string()))? 35 | .extract()?; 36 | 37 | let weight: u32 = dict 38 | .get_item("weight")? 39 | .ok_or_else(|| PyErr::new::("weight不能为空".to_string()))? 40 | .extract()?; 41 | 42 | let json: Option = dict 43 | .get_item("json")? 44 | .map(|value| depythonize(&value)) 45 | .transpose()?; 46 | 47 | let form_data: Option> = dict 48 | .get_item("form_data")? 49 | .map(|value| depythonize(&value)) 50 | .transpose()?; 51 | 52 | let headers: Option> = dict 53 | .get_item("headers")? 54 | .map(|value| depythonize(&value)) 55 | .transpose()?; 56 | 57 | let cookies: Option = dict 58 | .get_item("cookies")? 59 | .map(|value| depythonize(&value)) 60 | .transpose()?; 61 | 62 | let assert_options: Option> = dict 63 | .get_item("assert_options")? 64 | .map(|value| depythonize(&value)) 65 | .transpose()?; 66 | 67 | let think_time_option: Option = dict 68 | .get_item("think_time_option")? 69 | .map(|value| depythonize(&value)) 70 | .transpose()?; 71 | 72 | let setup_options_py = match dict.get_item("setup_options")? { 73 | Some(value) => Some(value.extract::>()?), 74 | None => None, 75 | }; 76 | 77 | let setup_options = utils::parse_setup_options::new(py, setup_options_py)?; 78 | 79 | let multipart_options_py = match dict.get_item("multipart_options")? { 80 | Some(value) => Some(value.extract::>()?), 81 | None => None, 82 | }; 83 | 84 | let multipart_options = 85 | utils::parse_multipart_options::new(py, multipart_options_py)?; 86 | 87 | endpoints.push(models::api_endpoint::ApiEndpoint { 88 | name, 89 | url, 90 | method, 91 | weight, 92 | json, 93 | form_data, 94 | multipart_options, 95 | headers, 96 | cookies, 97 | assert_options, 98 | think_time_option, 99 | setup_options, 100 | }); 101 | } 102 | Ok(endpoints) 103 | } 104 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_linux(musl).yml: -------------------------------------------------------------------------------- 1 | name: Build and publish Python wheels for musl/Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | - release-rc.* 8 | 9 | jobs: 10 | build-wheels: 11 | name: Build wheels on Alpine 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 16 | 17 | steps: 18 | - name: 签出代码 19 | uses: actions/checkout@v2 20 | 21 | - name: 签出前端 22 | uses: actions/checkout@v2 23 | with: 24 | repository: GiantAxeWhy/atomic-bomb-engine-front 25 | path: vue-project 26 | 27 | - name: 启动 Alpine 容器 28 | run: | 29 | docker run --name python-alpine-container -d -v "${{ github.workspace }}:/__w" -w "/__w" alpine:latest sleep 3600 30 | 31 | - name: 安装 Node.js 和构建工具 32 | run: | 33 | docker exec python-alpine-container apk add --no-cache nodejs npm build-base 34 | 35 | - name: 构建前端 36 | run: | 37 | docker exec python-alpine-container sh -c "cd vue-project && npm install && npm run build" 38 | 39 | - name: 安装 pyenv 和指定版本的 Python 40 | run: | 41 | docker exec python-alpine-container apk add --no-cache bash git curl openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev 42 | docker exec python-alpine-container sh -c "curl https://pyenv.run | bash" 43 | docker exec python-alpine-container sh -c 'export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" && for version in ${{ join(matrix.python-version, ' ') }}; do pyenv install $version; pyenv global $version; curl https://bootstrap.pypa.io/get-pip.py | python; done' 44 | 45 | - name: 安装 Rust、maturin 和 patchelf 46 | run: | 47 | docker exec python-alpine-container apk add --no-cache rust cargo patchelf 48 | docker exec python-alpine-container sh -c 'export PATH="/root/.pyenv/shims:$PATH" && pip install maturin' 49 | 50 | - name: 使用 maturin 构建 wheels 51 | run: | 52 | docker exec python-alpine-container sh -c ' 53 | export PATH="/root/.pyenv/bin:$PATH" && eval "$(pyenv init --path)" 54 | for version in ${{ join(matrix.python-version, ' ') }}; do 55 | pyenv global $version 56 | maturin build --release 57 | done' 58 | 59 | - name: 复制 wheels 到 Runner 60 | run: docker cp python-alpine-container:/__w/target/wheels ./ 61 | 62 | - name: 去除不兼容的 License 元数据 63 | run: | 64 | python - <<'PY' 65 | import pathlib 66 | import zipfile 67 | 68 | wheels_dir = pathlib.Path("wheels") 69 | for wheel_path in wheels_dir.glob("*.whl"): 70 | tmp_path = wheel_path.with_suffix(".tmp") 71 | with zipfile.ZipFile(wheel_path) as src, zipfile.ZipFile(tmp_path, "w") as dst: 72 | for info in src.infolist(): 73 | data = src.read(info.filename) 74 | if info.filename.endswith(".dist-info/METADATA"): 75 | text = data.decode("utf-8") 76 | new_lines = [] 77 | skip = False 78 | for line in text.splitlines(): 79 | lower = line.lower() 80 | if lower.startswith("license-file:") or lower.startswith("license-expression:"): 81 | continue 82 | new_lines.append(line) 83 | data = ("\n".join(new_lines) + "\n").encode("utf-8") 84 | dst.writestr(info, data) 85 | tmp_path.replace(wheel_path) 86 | PY 87 | 88 | - name: 安装 Twine 89 | run: pip install twine 90 | 91 | - name: 上传 wheels 到 PyPI 92 | env: 93 | TWINE_USERNAME: __token__ 94 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 95 | run: twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ wheels/*.whl 96 | -------------------------------------------------------------------------------- /python/atomic_bomb_engine/__init__.pyi: -------------------------------------------------------------------------------- 1 | from typing import Iterator, Optional, List, Dict, Any 2 | 3 | def assert_option(jsonpath: str, reference_object: any) -> Dict[str, Any]: 4 | """ 5 | 生成assert option 6 | :param jsonpath: jsonpath取值地址 7 | :param reference_object: 断言的值 8 | """ 9 | 10 | 11 | def step_option(increase_step: int, increase_interval: int) -> Dict[str, int]: 12 | """ 13 | 生成step option 14 | :param increase_step: 阶梯步长 15 | :param increase_interval: 阶梯间隔 16 | """ 17 | 18 | def think_time_option(min_millis: int, max_millis: int) -> Dict[str, int]: 19 | """ 20 | 思考时间选项 21 | :param min_millis: 22 | :param max_millis: 23 | :return: 24 | """ 25 | 26 | def endpoint( 27 | name: str, 28 | url: str, 29 | method: str, 30 | weight: int, 31 | json: Dict | None = None, 32 | form_data: Dict | None = None, 33 | multipart_options: List[Dict]| None = None, 34 | headers: Dict | None = None, 35 | cookies: str | None = None, 36 | assert_options: List | None = None, 37 | think_time_option: Dict[str, int] | None = None, 38 | setup_options: List| None = None, 39 | ) -> Dict[str, Any]: 40 | """ 41 | 生成endpoint 42 | :param assert_options: 43 | :param form_data: 44 | :param name: 接口名称 45 | :param url: 接口地址 46 | :param method: 请求方法 47 | :param weight 权重 48 | :param json: 请求json 49 | :param form_data: 请求form表单 50 | :multipart_options: 附件 51 | :param headers: 请求头 52 | :param cookies: cookie 53 | :param assert_options: 断言参数 54 | :param think_time_option: 思考时间 55 | :param setup_options: 接口初始化选项 56 | """ 57 | 58 | 59 | def setup_option( 60 | name: str, 61 | url: str, 62 | method: str, 63 | json: Dict| None = None, 64 | form_data: Dict| None = None, 65 | multipart_options: List[Dict]| None = None, 66 | headers: Dict| None = None, 67 | cookies: str | None = None, 68 | jsonpath_extract: List| None = None) ->Dict[str, Any]: 69 | """ 70 | 初始化选项 71 | :param name: 接口名称 72 | :param url: 接口地址 73 | :param method: 请求方法 74 | :param json: 请求json 75 | :param form_data: 请求form表单 76 | :multipart_options: 附件 77 | :param headers: 请求头 78 | :param cookies: cookie 79 | :param jsonpath_extract: 通过jsonpath提取参数 80 | :return: 81 | """ 82 | 83 | def jsonpath_extract_option(key: str, jsonpath: str) -> Dict[str, str]: 84 | """ 85 | jsonpath提取参数设置 86 | :param key: 全局key 87 | :param jsonpath: 提取jsonpath路径 88 | :return: 89 | """ 90 | 91 | def multipart_option( 92 | form_key: str, 93 | path: str, 94 | file_name: str, 95 | mime: str) -> Dict: 96 | """ 97 | 上传附件选项 98 | :param form_key: form表单的key,根据服务端选择,e.g: file, file1 99 | :param path: 文件路径 100 | :param file_name: 文件名 101 | :param mime: 文件类型,e.g: application/octet-stream,可以参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types 102 | """ 103 | 104 | class BatchRunner: 105 | def __init__(self) -> None: 106 | ... 107 | 108 | def run( 109 | self, 110 | test_duration_secs: int, 111 | concurrent_requests: int, 112 | api_endpoints:List[Dict], 113 | step_option:Dict[str, int]|None=None, 114 | setup_options:List[Dict[str, Any]]|None=None, 115 | verbose:bool=False, 116 | should_prevent:bool=False, 117 | assert_channel_buffer_size:int=1024, 118 | timeout_secs=0, 119 | cookie_store_enable=True, 120 | ema_alpha: float=0, 121 | ) -> None: 122 | """ 123 | 批量压测 124 | :param test_duration_secs: 测试持续时间 125 | :param concurrent_requests: 并发数 126 | :param api_endpoints: 接口信息 127 | :param step_option: 阶梯加压选项 128 | :param setup_options: 初始化选项 129 | :param verbose: 打印详细信息 130 | :param should_prevent: 是否禁用睡眠 131 | :param assert_channel_buffer_size: 断言队列buffer大小 132 | :param timeout_secs: http超时时间 133 | :param cookie_store_enable: 是否为客户端启用持久性cookie存储。 134 | :param ema_alpha: 指数滑动平均参数,0-1之间,0为不使用,值越大曲线越平滑,但是越失真,建议使用0.1以下 135 | """ 136 | ... 137 | 138 | def __iter__(self) -> 'BatchRunner': 139 | ... 140 | 141 | def __next__(self) -> Optional[Any]: 142 | ... -------------------------------------------------------------------------------- /python/atomic_bomb_engine/server.py: -------------------------------------------------------------------------------- 1 | import atomic_bomb_engine 2 | import os 3 | import sys 4 | import asyncio 5 | import webbrowser 6 | import time 7 | import aiohttp 8 | import aiosqlite 9 | import json 10 | from typing import Dict 11 | from aiohttp import web 12 | from atomic_bomb_engine import middleware 13 | 14 | 15 | def ui(port: int = 8000, auto_open: bool = True): 16 | if port > 65535 or port < 0: 17 | raise ValueError(f"端口必须为0-65535") 18 | # 数据库连接 19 | db_connection = None 20 | 21 | # 运行锁 22 | class RunningState: 23 | def __init__(self): 24 | self._running = False 25 | self._lock = asyncio.Lock() 26 | 27 | async def set_running(self): 28 | async with self._lock: 29 | self._running = True 30 | 31 | async def is_running(self) -> bool: 32 | async with self._lock: 33 | return self._running 34 | 35 | running = RunningState() 36 | 37 | async def get_db_connection(): 38 | nonlocal db_connection 39 | if db_connection is None: 40 | db_connection = await aiosqlite.connect(":memory:") 41 | await db_connection.execute('CREATE TABLE results (id INTEGER PRIMARY KEY, data JSON)') 42 | return db_connection 43 | 44 | async def create_table(): 45 | db = await get_db_connection() 46 | await db.commit() 47 | 48 | async def insert_result_data(data): 49 | db = await get_db_connection() 50 | json_data = json.dumps(data) 51 | await db.execute('INSERT INTO results (data) VALUES (?)', (json_data,)) 52 | await db.commit() 53 | 54 | async def fetch_all_result_data(): 55 | db = await get_db_connection() 56 | cursor = await db.execute('SELECT data FROM results ORDER BY id ASC') 57 | rows = await cursor.fetchall() 58 | results = [json.loads(row[0]) for row in rows] 59 | return results 60 | 61 | class WsConn: 62 | """连接池对象""" 63 | 64 | def __init__(self, ws: aiohttp.web_ws.WebSocketResponse, heartbeat_time: float): 65 | self.ws = ws 66 | self.heartbeat_time = heartbeat_time 67 | 68 | # ws连接池 69 | connections: Dict[str, WsConn] = dict() 70 | 71 | # 结果迭代器 72 | res_iter = None 73 | 74 | def decorator(func): 75 | async def start_service(*args, **kwargs): 76 | # 建表 77 | await create_table() 78 | 79 | # 定义ws接口 80 | async def websocket_handler(request): 81 | # 获取id 82 | if (client_id := request.match_info.get("id", None)) is None: 83 | return web.Response(status=400, text="缺少id参数") 84 | 85 | ws = web.WebSocketResponse() 86 | await ws.prepare(request) 87 | # 将id加入连接池 88 | connections[client_id] = WsConn(ws, time.time()) 89 | 90 | # 心跳检测 91 | async def check_heartbeat(): 92 | while True: 93 | await asyncio.sleep(0.5) 94 | if (ws_conn := connections.get(client_id)) is None: 95 | break 96 | if time.time() - ws_conn.heartbeat_time > 5: 97 | sys.stderr.write(f"{time.ctime()}客户端{client_id} 未发送心跳,断开连接\n") 98 | sys.stderr.flush() 99 | connections.pop(client_id, None) 100 | await ws_conn.ws.close() 101 | break 102 | 103 | async def push_result(): 104 | nonlocal res_iter 105 | 106 | while res_iter is None: 107 | await asyncio.sleep(0.1) 108 | 109 | for item in res_iter: 110 | if item: 111 | # 插入数据 112 | await insert_result_data(item) 113 | # 推送result 114 | for cid, conn in list(connections.items()): 115 | try: 116 | await conn.ws.send_json(item) 117 | except ConnectionResetError: 118 | sys.stderr.write(f'{time.ctime()}-WebSocket ID {cid} 断开, 无法推送\n') 119 | sys.stderr.flush() 120 | # 从连接池中移除断开的连接 121 | connections.pop(cid, None) 122 | return 123 | 124 | for cid, conn in list(connections.items()): 125 | try: 126 | await conn.ws.send_str("DONE") 127 | except ConnectionResetError: 128 | sys.stderr.write(f'{time.ctime()}-WebSocket ID {cid} 断开, 无法推送\n') 129 | sys.stderr.flush() 130 | # 从连接池中移除断开的连接 131 | connections.pop(cid, None) 132 | return 133 | 134 | # 推送任务 135 | push_task = asyncio.create_task(push_result()) 136 | # 心跳任务 137 | check_heartbeat_task = asyncio.create_task(check_heartbeat()) 138 | 139 | async for msg in ws: 140 | if msg.type is web.WSMsgType.TEXT: 141 | if msg.data.upper() == "PING": 142 | # 更新心跳时间 143 | if (ws_conn := connections.get(client_id, None)) is not None: 144 | ws_conn.heartbeat_time = time.time() 145 | await ws.send_str("PONG") 146 | elif msg.type is web.WSMsgType.ERROR: 147 | sys.stderr.write(f'WebSocket连接错误{ws.exception()}\n') 148 | sys.stderr.flush() 149 | 150 | await push_task 151 | sys.stderr.write('WebSocket连接关闭\n') 152 | sys.stderr.flush() 153 | 154 | await check_heartbeat_task 155 | connections.pop(client_id, None) 156 | return ws 157 | 158 | # 定义run接口 159 | async def run_decorated_function(request): 160 | if await running.is_running(): 161 | return web.json_response({"message": "任务正在运行中", "success": False}) 162 | await running.set_running() 163 | nonlocal res_iter 164 | res_iter = await func(*args, **kwargs) 165 | return web.json_response({"message": "压测任务已启动", "success": True}) 166 | 167 | # 定义history接口 168 | async def history(request): 169 | results = await fetch_all_result_data() 170 | return web.json_response(results) 171 | 172 | # 重定向到首页 173 | async def redirect_to_index(request): 174 | response = web.HTTPFound('/static/index.html') 175 | # 禁用缓存 176 | response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' 177 | response.headers['Pragma'] = 'no-cache' 178 | response.headers['Expires'] = '0' 179 | return response 180 | 181 | app = web.Application(middlewares=[middleware.cors_middleware]) 182 | # 静态页面 183 | app.router.add_static('/static', path=os.path.join(os.path.dirname(__file__), 'dist'), name='dist') 184 | # 路由 185 | app.add_routes([web.get('/', redirect_to_index), 186 | web.get('/ws/{id}', websocket_handler), 187 | web.get('/run', run_decorated_function), 188 | web.get('/history', history), 189 | ]) 190 | runner = web.AppRunner(app) 191 | await runner.setup() 192 | site = web.TCPSite(runner, '0.0.0.0', port) 193 | await site.start() 194 | # 等待协程运行完成 195 | await asyncio.Event().wait() 196 | 197 | sys.stderr.write(f"服务启动成功: http://localhost:{port}\n") 198 | sys.stderr.flush() 199 | if auto_open: 200 | webbrowser.open(f"http://localhost:{port}") 201 | return start_service 202 | 203 | return decorator 204 | -------------------------------------------------------------------------------- /src/py_lib/batch_runner.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | use atomic_bomb_engine::models::result::BatchResult; 3 | use futures::stream::BoxStream; 4 | use futures::StreamExt; 5 | use pyo3::types::{PyAny, PyDict, PyDictMethods, PyList}; 6 | use pyo3::{pyclass, pymethods, Py, PyRefMut, PyResult, Python}; 7 | use std::sync::Arc; 8 | use tokio::sync::Mutex; 9 | 10 | #[pyclass] 11 | pub(crate) struct BatchRunner { 12 | runtime: tokio::runtime::Runtime, 13 | stream: Arc, anyhow::Error>>>>>, 14 | is_done: Arc>, 15 | } 16 | 17 | #[pymethods] 18 | impl BatchRunner { 19 | #[new] 20 | fn new() -> Self { 21 | BatchRunner { 22 | runtime: tokio::runtime::Runtime::new().unwrap(), 23 | stream: Arc::new(Mutex::new(None)), 24 | is_done: Arc::new(Mutex::new(false)), 25 | } 26 | } 27 | 28 | #[pyo3(signature = ( 29 | test_duration_secs, 30 | concurrent_requests, 31 | api_endpoints, 32 | step_option=None, 33 | setup_options=None, 34 | verbose=false, 35 | should_prevent=false, 36 | assert_channel_buffer_size=1024, 37 | timeout_secs=0, 38 | cookie_store_enable=true, 39 | ema_alpha=0f64, 40 | ))] 41 | fn run( 42 | &self, 43 | py: Python, 44 | test_duration_secs: u64, 45 | concurrent_requests: usize, 46 | api_endpoints: Py, 47 | step_option: Option>, 48 | setup_options: Option>, 49 | verbose: bool, 50 | should_prevent: bool, 51 | assert_channel_buffer_size: usize, 52 | timeout_secs: u64, 53 | cookie_store_enable: bool, 54 | ema_alpha: f64, 55 | ) -> PyResult> { 56 | let stream_clone = self.stream.clone(); 57 | let endpoints = utils::parse_api_endpoints::new(py, api_endpoints) 58 | .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; 59 | let step_opt = utils::parse_step_options::new(py, step_option) 60 | .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; 61 | let setup_opts = utils::parse_setup_options::new(py, setup_options) 62 | .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?; 63 | 64 | let fut = async move { 65 | let stream = atomic_bomb_engine::core::run_batch::run_batch( 66 | test_duration_secs, 67 | concurrent_requests, 68 | timeout_secs, 69 | cookie_store_enable, 70 | verbose, 71 | should_prevent, 72 | endpoints, 73 | step_opt, 74 | setup_opts, 75 | assert_channel_buffer_size, 76 | ema_alpha, 77 | ) 78 | .await; 79 | *stream_clone.lock().await = Some(stream); 80 | Ok::<(), pyo3::PyErr>(()) 81 | }; 82 | 83 | Python::with_gil(|py| { 84 | pyo3_async_runtimes::tokio::future_into_py(py, fut) 85 | .map(|py_any| py_any.unbind()) 86 | }) 87 | } 88 | 89 | fn __iter__(slf: PyRefMut) -> PyResult> { 90 | Ok(slf) 91 | } 92 | 93 | fn __next__(slf: PyRefMut<'_, Self>, py: Python) -> PyResult>> { 94 | let is_done_clone = slf.is_done.clone(); 95 | 96 | let mut stream_guard = slf.runtime.block_on(async { 97 | let stream = slf.stream.lock().await; 98 | stream 99 | }); 100 | 101 | let is_done = slf.runtime.block_on(async { 102 | let done = is_done_clone.lock().await; 103 | *done 104 | }); 105 | 106 | if is_done { 107 | return Ok(None); 108 | } 109 | 110 | match stream_guard.as_mut() { 111 | Some(stream) => { 112 | let next_stream = slf.runtime.block_on(async { 113 | let batch_result = stream.next().await; 114 | batch_result 115 | }); 116 | 117 | match next_stream { 118 | Some(Ok(result)) => { 119 | if result.is_none() { 120 | let done = slf.is_done.clone(); 121 | slf.runtime.block_on(async { 122 | let mut done_lock = done.lock().await; 123 | *done_lock = true; 124 | }); 125 | } 126 | 127 | let dict = PyDict::new(py); 128 | if let Some(test_result) = result { 129 | dict.set_item("total_duration", test_result.total_duration)?; 130 | dict.set_item("success_rate", test_result.success_rate)?; 131 | dict.set_item("error_rate", test_result.error_rate)?; 132 | dict.set_item( 133 | "median_response_time", 134 | test_result.median_response_time, 135 | )?; 136 | dict.set_item("response_time_95", test_result.response_time_95)?; 137 | dict.set_item("response_time_99", test_result.response_time_99)?; 138 | dict.set_item("total_requests", test_result.total_requests)?; 139 | dict.set_item("rps", test_result.rps)?; 140 | dict.set_item("max_response_time", test_result.max_response_time)?; 141 | dict.set_item("min_response_time", test_result.min_response_time)?; 142 | dict.set_item("err_count", test_result.err_count)?; 143 | dict.set_item("total_data_kb", test_result.total_data_kb)?; 144 | dict.set_item( 145 | "throughput_per_second_kb", 146 | test_result.throughput_per_second_kb, 147 | )?; 148 | let http_error_list = utils::create_http_err_dict::create_http_error_dict( 149 | py, 150 | &test_result.http_errors, 151 | )?; 152 | dict.set_item("http_errors", http_error_list)?; 153 | let assert_error_list = utils::create_assert_err_dict::create_assert_error_dict( 154 | py, 155 | &test_result.assert_errors, 156 | )?; 157 | dict.set_item("assert_errors", assert_error_list)?; 158 | dict.set_item("timestamp", test_result.timestamp)?; 159 | let api_results = utils::create_api_results_dict::create_api_results_dict( 160 | py, 161 | test_result.api_results, 162 | )?; 163 | dict.set_item("api_results", api_results)?; 164 | dict.set_item( 165 | "total_concurrent_number", 166 | test_result.total_concurrent_number, 167 | )?; 168 | dict.set_item("errors_per_second", test_result.errors_per_second)?; 169 | }; 170 | Ok(Some(dict.into_any().unbind())) 171 | } 172 | Some(Err(e)) => Err(pyo3::exceptions::PyRuntimeError::new_err(e.to_string())), 173 | None => { 174 | let done = slf.is_done.clone(); 175 | slf.runtime.block_on(async { 176 | let mut done_lock = done.lock().await; 177 | *done_lock = true; 178 | }); 179 | Ok(None) 180 | } 181 | } 182 | } 183 | None => { 184 | eprintln!("stream未初始化,请等待"); 185 | let dict = PyDict::new(py); 186 | dict.set_item("should_wait", true)?; 187 | Ok(Some(dict.into_any().unbind())) 188 | } 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atomic-bomb-engine-py 2 | #### [atomic-bomb-engine](https://github.com/we-lsp/atomic-bomb-engine)的python包装实现 3 | 4 | logo 5 | 6 | 7 | ## 前端仓库 8 | #### [atomic-bomb-engine-front](https://github.com/GiantAxeWhy/atomic-bomb-engine-front) 9 | 10 | ## 使用条件: 11 | - python版本 >= 3.8(已验证 3.8 - 3.13) 12 | - windows(x86), linux(x86), mac 13 | 14 | ## 使用方法: 15 | - ### 准备开始 16 | 通过pip安装 (0.5.0版本之前) 17 | ```shell 18 | pip install atomic-bomb-engine-py 19 | ``` 20 | 在python中引用时注意,需要引用atomic_bomb_engine, 而不是atomic_bomb_engine_py 21 |
为了避免混淆,0.5.0版本之后,pip更换了包名,更改为atomic-bomb-engine, 22 | ```shell 23 | pip install atomic-bomb-engine 24 | ``` 25 | 在python中导入 26 | ```python 27 | import atomic_bomb_engine 28 | ``` 29 | 异步使用的时候,还需要引用asyncio 30 | ```python 31 | import asyncio 32 | ``` 33 | - ### 开始压测 34 | - ~~单接口压测~~ (功能与多接口压测重叠,已废除) 35 | 36 | - 多接口压测 37 | 38 | 多接口压测可以先使用 39 | ```python 40 | runner = atomic_bomb_engine.BatchRunner() 41 | ``` 42 | 实例化一个runner类 43 | 通过runner类中的run方法开启压测 44 | run方法函数签名如下 45 | ```python 46 | def run( 47 | self, 48 | test_duration_secs: int, 49 | concurrent_requests: int, 50 | api_endpoints:List[Dict], 51 | step_option:Dict[str, int]|None=None, 52 | setup_options:List[Dict[str, Any]]|None=None, 53 | verbose:bool=False, 54 | should_prevent:bool=False, 55 | assert_channel_buffer_size:int=1024, 56 | timeout_secs=0, 57 | cookie_store_enable=True 58 | ) -> None: 59 | """ 60 | 批量压测 61 | :param test_duration_secs: 测试持续时间 62 | :param concurrent_requests: 并发数 63 | :param api_endpoints: 接口信息 64 | :param step_option: 阶梯加压选项 65 | :param setup_options: 初始化选项 66 | :param verbose: 打印详细信息 67 | :param should_prevent: 是否禁用睡眠 68 | :param assert_channel_buffer_size: 断言队列buffer大小 69 | :param timeout_secs: http超时时间 70 | :param cookie_store_enable: 是否为客户端启用持久性cookie存储。 71 | """ 72 | ``` 73 | 74 | 使用assert_option方法可以返回断言选项字典 75 | ```python 76 | assert_options=[ 77 | atomic_bomb_engine.assert_option("$.code", 429), 78 | atomic_bomb_engine.assert_option("$.code", 200) 79 | ]) 80 | ``` 81 | jsonpath如果不会用的话,建议去[jsonpath](https://jsonpath.com/)学习 82 | 83 | 使用step_option方法可以返回阶梯加压选项字典 84 | ```python 85 | def step_option(increase_step: int, increase_interval: int) -> Dict[str, int]: 86 | """ 87 | 生成step option 88 | :param increase_step: 阶梯步长 89 | :param increase_interval: 阶梯间隔 90 | """ 91 | ``` 92 | 93 | 同样的本包中也包含了一个对api_endpoint的包装:endpoint方法,方便调用,endpoint中的assert_options中也可以套用assert_option方法 94 | ```python 95 | async def run_batch(): 96 | result = await atomic_bomb_engine.batch_async( 97 | # 测试持续时间 98 | test_duration_secs=60, 99 | # 并发量 100 | concurrent_requests=200, 101 | # 阶梯设置(每5秒增加30个并发) 102 | step_option=atomic_bomb_engine.step_option(increase_step=30, increase_interval=5), 103 | # 接口超时时间 104 | timeout_secs=10, 105 | # 是否开启客户端启用持久性cookie存储 106 | cookie_store_enable=True, 107 | # 全局初始化 108 | setup_options=[ 109 | atomic_bomb_engine.setup_option( 110 | name="初始化-1", 111 | url="http://localhost:8080/setup", 112 | method="get", 113 | jsonpath_extract=[ 114 | atomic_bomb_engine.jsonpath_extract_option(key="test-msg", jsonpath="$.msg"), 115 | atomic_bomb_engine.jsonpath_extract_option(key="test-code", jsonpath="$.code"), 116 | ] 117 | )], 118 | # 是否开启详细日志 119 | verbose=False, 120 | # 被压接口设置 121 | api_endpoints=[ 122 | atomic_bomb_engine.endpoint( 123 | # 接口任务命名 124 | name="test-1", 125 | # 针对每个接口初始化 126 | setup_options=[ 127 | atomic_bomb_engine.setup_option( 128 | name="api-初始化-1", 129 | url="http://localhost:8080/api_setup", 130 | method="get", 131 | jsonpath_extract=[ 132 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-msg-1", jsonpath="$.msg"), 133 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-code-1", jsonpath="$.code"), 134 | ] 135 | ) 136 | ], 137 | # 被压接口url 138 | url="http://localhost:8080/direct", 139 | # 请求方式 140 | method="POST", 141 | # 权重 142 | weight=1, 143 | # 发送json请求 144 | json={"name": "{{api-test-msg-1}}", "number": 1}, 145 | # 断言选项 146 | assert_options=[ 147 | atomic_bomb_engine.assert_option(jsonpath="$.number", reference_object=1), 148 | ], 149 | # 思考时间选项(在最大和最小之间随机,单位毫秒) 150 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=500, max_millis=1200), 151 | ), 152 | ]) 153 | print(result) 154 | return result 155 | ``` 156 | 157 | 监听时可以在使用完run方法后,继续迭代runner即可 158 | 159 | 压测+同时监听 160 | 161 | ```python 162 | import asyncio 163 | 164 | 165 | async def batch_async(): 166 | runner = atomic_bomb_engine.BatchRunner() 167 | runner.run( 168 | test_duration_secs=30, 169 | concurrent_requests=30, 170 | step_option=atomic_bomb_engine.step_option(increase_step=3, increase_interval=3), 171 | timeout_secs=15, 172 | cookie_store_enable=True, 173 | verbose=False, 174 | api_endpoints=[ 175 | atomic_bomb_engine.endpoint( 176 | name="test-1", 177 | url="http://127.0.0.1:8080/direct", 178 | method="POST", 179 | json={"name": "test-1", "number": 1}, 180 | weight=100, 181 | assert_options=[ 182 | atomic_bomb_engine.assert_option(jsonpath="$.msg", reference_object="操作成功"), 183 | ], 184 | ), 185 | ]) 186 | return runner 187 | 188 | 189 | async def main(): 190 | results = await batch_async() 191 | for res in results: 192 | if res.get("should_wait"): 193 | await asyncio.sleep(0.1) 194 | print(res) 195 | 196 | 197 | if __name__ == '__main__': 198 | asyncio.run(main()) 199 | ``` 200 | 201 | # 压测时使用ui界面监控 202 | 203 | 0.5.0版本后,添加了ui页面,支持批量压测方法 204 |
导入 205 | ```python 206 | from atomic_bomb_engine import server 207 | ``` 208 | 使用 209 | ```python 210 | import asyncio 211 | 212 | import atomic_bomb_engine 213 | from atomic_bomb_engine import server 214 | 215 | 216 | @server.ui(port=8000) 217 | async def batch_async(): 218 | runner = atomic_bomb_engine.BatchRunner() 219 | runner.run( 220 | # 测试持续时间 221 | test_duration_secs=60, 222 | # 并发量 223 | concurrent_requests=200, 224 | # 阶梯设置(每5秒增加30个并发) 225 | step_option=atomic_bomb_engine.step_option(increase_step=30, increase_interval=5), 226 | # 接口超时时间 227 | timeout_secs=10, 228 | # 是否开启客户端启用持久性cookie存储 229 | cookie_store_enable=True, 230 | # 全局初始化 231 | setup_options=[ 232 | atomic_bomb_engine.setup_option( 233 | name="初始化-1", 234 | url="http://localhost:8080/setup", 235 | method="get", 236 | jsonpath_extract=[ 237 | atomic_bomb_engine.jsonpath_extract_option(key="test-msg", jsonpath="$.msg"), 238 | atomic_bomb_engine.jsonpath_extract_option(key="test-code", jsonpath="$.code"), 239 | ] 240 | )], 241 | # 是否开启详细日志 242 | verbose=False, 243 | # 被压接口设置 244 | api_endpoints=[ 245 | atomic_bomb_engine.endpoint( 246 | # 接口任务命名 247 | name="test-1", 248 | # 针对每个接口初始化 249 | setup_options=[ 250 | atomic_bomb_engine.setup_option( 251 | name="api-初始化-1", 252 | url="http://localhost:8080/api_setup", 253 | method="get", 254 | jsonpath_extract=[ 255 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-msg-1", jsonpath="$.msg"), 256 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-code-1", jsonpath="$.code"), 257 | ] 258 | ) 259 | ], 260 | # 被压接口url 261 | url="http://localhost:8080/direct", 262 | # 请求方式 263 | method="POST", 264 | # 权重 265 | weight=1, 266 | # 发送json请求 267 | json={"name": "{{api-test-msg-1}}", "number": 1}, 268 | # 断言选项 269 | assert_options=[ 270 | atomic_bomb_engine.assert_option(jsonpath="$.number", reference_object=1), 271 | ], 272 | # 思考时间选项(在最大和最小之间随机,单位毫秒) 273 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=500, max_millis=1200), 274 | ), 275 | ]) 276 | return runner 277 | 278 | 279 | if __name__ == '__main__': 280 | asyncio.run(batch_async()) 281 | ``` 282 | 283 | 使用server.ui装饰器,可以给批量压测方法启动一个简单的web服务器,不需要再手动监听BatchListenIter生成器 284 | 285 | ## 内部架构图 286 | ![architecture.png](img/architecture.png) 287 | 288 | ## [0.19.0] - 2024-04-16 289 | ### Added 290 | - 增加了初始化和参数模版功能 291 | ```python 292 | setup_options=[ 293 | atomic_bomb_engine.setup_option( 294 | name="初始化-1", 295 | url="http://localhost:8080/setup", 296 | method="get", 297 | jsonpath_extract=[ 298 | atomic_bomb_engine.jsonpath_extract_option(key="test-msg", jsonpath="$.msg"), 299 | atomic_bomb_engine.jsonpath_extract_option(key="test-code", jsonpath="$.code"), 300 | ] 301 | )] 302 | ``` 303 | 上述实例展示了如何在初始化的时候调用某个接口,并且通过jsonpath将数据提取出来,保存在全局变量test-msg和test-code中 304 | 提取完全局变量后,就可以在后续的api_endpoints中使用 305 | ```python 306 | api_endpoints=[ 307 | atomic_bomb_engine.endpoint( 308 | # 接口任务命名 309 | name="test-1", 310 | # 针对每个接口初始化 311 | setup_options=[ 312 | atomic_bomb_engine.setup_option( 313 | name="api-初始化-1", 314 | url="http://localhost:8080/api_setup", 315 | method="get", 316 | jsonpath_extract=[ 317 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-msg-1", jsonpath="$.msg"), 318 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-code-1", jsonpath="$.code"), 319 | ] 320 | ) 321 | ], 322 | # 被压接口url 323 | url="http://localhost:8080/direct", 324 | # 请求方式 325 | method="POST", 326 | # 权重 327 | weight=1, 328 | # 发送json请求 329 | json={"name": "{{api-test-msg-1}}", "number": 1}, 330 | # 断言选项 331 | assert_options=[ 332 | atomic_bomb_engine.assert_option(jsonpath="$.number", reference_object=1), 333 | ], 334 | # 思考时间选项(在最大和最小之间随机,单位毫秒) 335 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=500, max_millis=1200), 336 | ), 337 | ] 338 | ``` 339 | 上述实例展示了如何在请求中使用全局变量,使用双大括号即可使用 340 | 341 | ### Fixed 342 | - 修复了如果http状态码错误时,不会记录 343 | - 修复了json反序列化的问题 344 | 345 | ## [0.20.0] - 2024-04-17 346 | ### Added 347 | 断言更改为异步生产消费,提升性能 348 | 349 | ## bug和需求 350 | - 如果发现了bug,把复现步骤一起写到Issus中哈 351 | - 如果有需求也可以在Issues中讨论 352 | - 本程序是本人业余时间开发,不太准备保证时效性,但是如果有时间,一定第一时间回复和修改bug 353 | 354 | ## [0.22.0] - 2024-04-18 355 | ### Added 356 | 前端进行了性能优化 357 | 358 | ## [0.24.0] - 2024-04-22 359 | ### Added 360 | 异步断言使用了补偿消息,保证消息的一致性 361 | 362 | ## [0.25.0] - 2024-04-23 363 | ### Added 364 | 在endpoints中增加思考时间,模拟用户行为 365 | ```python 366 | think_time_option(min_millis=200, max_millis=300) 367 | ``` 368 | - min_millis:最小思考时间(毫秒) 369 | - max_millis:最大思考时间(毫秒) 370 | 371 | 使用时在endpoint中增加think_time_option参数 372 | 373 | ```python 374 | api_endpoints=[ 375 | atomic_bomb_engine.endpoint( 376 | name="test-1", 377 | url="http://localhost:8080/a", 378 | method="POST", 379 | weight=1, 380 | timeout_secs=10, 381 | json={"name": "{{test-msg}}", "number": "{{test-code}}"}, 382 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=200, max_millis=300), 383 | ), 384 | ] 385 | ``` 386 | 387 | ## [0.26.0] - 2024-04-24 388 | ### Added 389 | - 增加endpoint中的setup,在并发中可以做接口断言 390 | - 增加有关联条件下的cookie自动管理功能 391 | ```python 392 | atomic_bomb_engine.endpoint( 393 | # 接口任务命名 394 | name="test-1", 395 | # 针对每个接口初始化 396 | setup_options=[ 397 | atomic_bomb_engine.setup_option( 398 | name="api-初始化-1", 399 | url="http://localhost:8080/api_setup", 400 | method="get", 401 | jsonpath_extract=[ 402 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-msg-1", jsonpath="$.msg"), 403 | atomic_bomb_engine.jsonpath_extract_option(key="api-test-code-1", jsonpath="$.code"), 404 | ] 405 | ) 406 | ], 407 | # 被压接口url 408 | url="http://localhost:8080/direct", 409 | # 请求方式 410 | method="POST", 411 | # 权重 412 | weight=1, 413 | # 发送json请求 414 | json={"name": "{{api-test-msg-1}}", "number": 1}, 415 | # 断言选项 416 | assert_options=[ 417 | atomic_bomb_engine.assert_option(jsonpath="$.number", reference_object=1), 418 | ], 419 | # 思考时间选项(在最大和最小之间随机,单位毫秒) 420 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=500, max_millis=1200), 421 | ) 422 | ``` 423 | - 参数cookie_store_enable控制是否自动管理cookie,前置条件的cookie会带入到最终的压测接口中 424 | - 在endpoint中使用setup_options可以传入多个接口,并且提取参数 425 | - 提取到的参数如果和全局的setup的key冲突,会覆盖全局提取到的参数 426 | - 接口中提取的参数只能在本线程(v-user)中使用 427 | - ⚠️ 使用时注意:setup_options是顺序执行的,没有并发,但是相当于添加了think time 428 | 429 | ## [0.28.0] - 2024-04-25 430 | ### Added 431 | - 将持久化cookie添加到全局选项中 432 | - 复用http client 433 | - 选择性开启断言任务 434 | - 接口初始化时出现错误等待后重试## 435 | 436 | ## [0.29.0] - 2024-04-25 437 | ### Added 438 | - 优化并发逻辑 439 | - 前端更改为web worker发送心跳 440 | 441 | ## [0.38.0] - 2024-05-7 442 | ### Added 443 | - 增加附件上传功能 444 | - 在初始化和每个接口中增加了multipart_options参数用于附件上传 445 | - 增加multipart_option方法封装附件参数 446 | - form_key: form表单的key 447 | - path: 附件路径 448 | - file_name: 附件名 449 | - mime: 附件类型 (类型可以参考[这里](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types)) 450 | ```python 451 | api_endpoints=[ 452 | atomic_bomb_engine.endpoint( 453 | name="test-file", 454 | url="http://127.0.0.1:8888/upload", 455 | method="post", 456 | weight=100, 457 | multipart_options=[atomic_bomb_engine.multipart_option(form_key="file", path="./ui.py", file_name="ui.py", mime="text/plain")], 458 | assert_options=[ 459 | atomic_bomb_engine.assert_option(jsonpath="$.message", reference_object="File uploaded successfully!"), 460 | ], 461 | think_time_option=atomic_bomb_engine.think_time_option(min_millis=500, max_millis=1200), 462 | ),] 463 | ``` 464 | 465 | ## [0.39.0] - 2024-05-15 466 | ### Added 467 | - 启用BatchRunner类,每次执行可以返回一个迭代器 468 | - 废除run_batch方法 469 | - 废除ResultsIter迭代器 470 | 471 | ## [0.40.0] - 2024-05-16 472 | ### Added 473 | - 将rps统计改为滑动窗口的形式 474 | 475 | ## [0.41.0] - 2024-05-20 476 | ### Added 477 | - run方法增加指数滑动平均参数: ema_alpha 478 | - 参数为0-1之间的一个浮点数 479 | - 参数为0时不启用 480 | - 数值越大越平滑,但是失真越多 481 | - 建议使用0.1以下 482 | 483 | ## bug和需求 484 | - 如果发现了bug,把复现步骤一起写到Issus中哈 485 | - 如果有需求也可以在Issues中讨论 486 | - 本程序是本人业余时间开发,不太准备保证时效性,但是如果有时间,一定第一时间回复和修改bug 487 | 488 | ## TODO 489 | - [x] 前端展示页面 ✅ 490 | - [x] 接口关联 ✅ 491 | - [x] 每个接口可以配置思考时间 ✅ 492 | - [x] 增加form支持 ✅ 493 | - [ ] 增加代理支持 494 | - [x] 增加附件支持 ✅ 495 | - [ ] 断言支持不等于等更多表达方式 496 | 497 | ## 联系方式 498 | - 邮箱:[qyzhg@qyzhg.com](mailto:qyzhg@qyzhg.com) 499 | - 微信:qy-zhg 500 | 501 | ## 👏🏻👏🏻👏🏻欢迎加群交流 502 | ![img.png](img/img.png) 503 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.86" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 25 | 26 | [[package]] 27 | name = "atomic-bomb-engine" 28 | version = "0.41.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "47ec75935e08e27cc81da711141ffeef544c7872df1e37a2f912af0a6167cd75" 31 | dependencies = [ 32 | "anyhow", 33 | "futures", 34 | "handlebars", 35 | "histogram", 36 | "jsonpath_lib", 37 | "lazy_static", 38 | "os_info", 39 | "parking_lot", 40 | "rand", 41 | "reqwest", 42 | "serde", 43 | "serde_json", 44 | "time", 45 | "tokio", 46 | "url", 47 | "winapi", 48 | ] 49 | 50 | [[package]] 51 | name = "atomic-bomb-engine-py" 52 | version = "0.42.0" 53 | dependencies = [ 54 | "anyhow", 55 | "atomic-bomb-engine", 56 | "futures", 57 | "pyo3", 58 | "pyo3-async-runtimes", 59 | "serde", 60 | "serde_json", 61 | "tokio", 62 | ] 63 | 64 | [[package]] 65 | name = "atomic-waker" 66 | version = "1.1.2" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 69 | 70 | [[package]] 71 | name = "autocfg" 72 | version = "1.3.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 75 | 76 | [[package]] 77 | name = "backtrace" 78 | version = "0.3.71" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 81 | dependencies = [ 82 | "addr2line", 83 | "cc", 84 | "cfg-if", 85 | "libc", 86 | "miniz_oxide", 87 | "object", 88 | "rustc-demangle", 89 | ] 90 | 91 | [[package]] 92 | name = "base64" 93 | version = "0.22.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 96 | 97 | [[package]] 98 | name = "bitflags" 99 | version = "1.3.2" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 102 | 103 | [[package]] 104 | name = "bitflags" 105 | version = "2.5.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 108 | 109 | [[package]] 110 | name = "block-buffer" 111 | version = "0.10.4" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 114 | dependencies = [ 115 | "generic-array", 116 | ] 117 | 118 | [[package]] 119 | name = "bumpalo" 120 | version = "3.16.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 123 | 124 | [[package]] 125 | name = "bytes" 126 | version = "1.6.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 129 | 130 | [[package]] 131 | name = "cc" 132 | version = "1.0.98" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" 135 | 136 | [[package]] 137 | name = "cfg-if" 138 | version = "1.0.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 141 | 142 | [[package]] 143 | name = "cookie" 144 | version = "0.17.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" 147 | dependencies = [ 148 | "percent-encoding", 149 | "time", 150 | "version_check", 151 | ] 152 | 153 | [[package]] 154 | name = "cookie_store" 155 | version = "0.20.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" 158 | dependencies = [ 159 | "cookie", 160 | "idna 0.3.0", 161 | "log", 162 | "publicsuffix", 163 | "serde", 164 | "serde_derive", 165 | "serde_json", 166 | "time", 167 | "url", 168 | ] 169 | 170 | [[package]] 171 | name = "core-foundation" 172 | version = "0.9.4" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 175 | dependencies = [ 176 | "core-foundation-sys", 177 | "libc", 178 | ] 179 | 180 | [[package]] 181 | name = "core-foundation-sys" 182 | version = "0.8.6" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 185 | 186 | [[package]] 187 | name = "cpufeatures" 188 | version = "0.2.12" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 191 | dependencies = [ 192 | "libc", 193 | ] 194 | 195 | [[package]] 196 | name = "crypto-common" 197 | version = "0.1.6" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 200 | dependencies = [ 201 | "generic-array", 202 | "typenum", 203 | ] 204 | 205 | [[package]] 206 | name = "deranged" 207 | version = "0.3.11" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 210 | dependencies = [ 211 | "powerfmt", 212 | ] 213 | 214 | [[package]] 215 | name = "digest" 216 | version = "0.10.7" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 219 | dependencies = [ 220 | "block-buffer", 221 | "crypto-common", 222 | ] 223 | 224 | [[package]] 225 | name = "encoding_rs" 226 | version = "0.8.34" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 229 | dependencies = [ 230 | "cfg-if", 231 | ] 232 | 233 | [[package]] 234 | name = "equivalent" 235 | version = "1.0.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 238 | 239 | [[package]] 240 | name = "errno" 241 | version = "0.3.9" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 244 | dependencies = [ 245 | "libc", 246 | "windows-sys 0.52.0", 247 | ] 248 | 249 | [[package]] 250 | name = "fastrand" 251 | version = "2.1.0" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 254 | 255 | [[package]] 256 | name = "fnv" 257 | version = "1.0.7" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 260 | 261 | [[package]] 262 | name = "foreign-types" 263 | version = "0.3.2" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 266 | dependencies = [ 267 | "foreign-types-shared", 268 | ] 269 | 270 | [[package]] 271 | name = "foreign-types-shared" 272 | version = "0.1.1" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 275 | 276 | [[package]] 277 | name = "form_urlencoded" 278 | version = "1.2.1" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 281 | dependencies = [ 282 | "percent-encoding", 283 | ] 284 | 285 | [[package]] 286 | name = "futures" 287 | version = "0.3.30" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 290 | dependencies = [ 291 | "futures-channel", 292 | "futures-core", 293 | "futures-executor", 294 | "futures-io", 295 | "futures-sink", 296 | "futures-task", 297 | "futures-util", 298 | ] 299 | 300 | [[package]] 301 | name = "futures-channel" 302 | version = "0.3.30" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 305 | dependencies = [ 306 | "futures-core", 307 | "futures-sink", 308 | ] 309 | 310 | [[package]] 311 | name = "futures-core" 312 | version = "0.3.30" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 315 | 316 | [[package]] 317 | name = "futures-executor" 318 | version = "0.3.30" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 321 | dependencies = [ 322 | "futures-core", 323 | "futures-task", 324 | "futures-util", 325 | ] 326 | 327 | [[package]] 328 | name = "futures-io" 329 | version = "0.3.30" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 332 | 333 | [[package]] 334 | name = "futures-macro" 335 | version = "0.3.30" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 338 | dependencies = [ 339 | "proc-macro2", 340 | "quote", 341 | "syn", 342 | ] 343 | 344 | [[package]] 345 | name = "futures-sink" 346 | version = "0.3.30" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 349 | 350 | [[package]] 351 | name = "futures-task" 352 | version = "0.3.30" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 355 | 356 | [[package]] 357 | name = "futures-util" 358 | version = "0.3.30" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 361 | dependencies = [ 362 | "futures-channel", 363 | "futures-core", 364 | "futures-io", 365 | "futures-macro", 366 | "futures-sink", 367 | "futures-task", 368 | "memchr", 369 | "pin-project-lite", 370 | "pin-utils", 371 | "slab", 372 | ] 373 | 374 | [[package]] 375 | name = "generic-array" 376 | version = "0.14.7" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 379 | dependencies = [ 380 | "typenum", 381 | "version_check", 382 | ] 383 | 384 | [[package]] 385 | name = "getrandom" 386 | version = "0.2.15" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 389 | dependencies = [ 390 | "cfg-if", 391 | "libc", 392 | "wasi", 393 | ] 394 | 395 | [[package]] 396 | name = "gimli" 397 | version = "0.28.1" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 400 | 401 | [[package]] 402 | name = "h2" 403 | version = "0.4.5" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" 406 | dependencies = [ 407 | "atomic-waker", 408 | "bytes", 409 | "fnv", 410 | "futures-core", 411 | "futures-sink", 412 | "http", 413 | "indexmap", 414 | "slab", 415 | "tokio", 416 | "tokio-util", 417 | "tracing", 418 | ] 419 | 420 | [[package]] 421 | name = "handlebars" 422 | version = "5.1.2" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" 425 | dependencies = [ 426 | "log", 427 | "pest", 428 | "pest_derive", 429 | "serde", 430 | "serde_json", 431 | "thiserror", 432 | ] 433 | 434 | [[package]] 435 | name = "hashbrown" 436 | version = "0.14.5" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 439 | 440 | [[package]] 441 | name = "heck" 442 | version = "0.5.0" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 445 | 446 | [[package]] 447 | name = "hermit-abi" 448 | version = "0.3.9" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 451 | 452 | [[package]] 453 | name = "histogram" 454 | version = "0.10.1" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "5bd81cb9a629d0a868f2092937332fca5fd985971d2155bf6fcc225ef8a6be2c" 457 | dependencies = [ 458 | "thiserror", 459 | ] 460 | 461 | [[package]] 462 | name = "http" 463 | version = "1.1.0" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 466 | dependencies = [ 467 | "bytes", 468 | "fnv", 469 | "itoa", 470 | ] 471 | 472 | [[package]] 473 | name = "http-body" 474 | version = "1.0.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" 477 | dependencies = [ 478 | "bytes", 479 | "http", 480 | ] 481 | 482 | [[package]] 483 | name = "http-body-util" 484 | version = "0.1.1" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" 487 | dependencies = [ 488 | "bytes", 489 | "futures-core", 490 | "http", 491 | "http-body", 492 | "pin-project-lite", 493 | ] 494 | 495 | [[package]] 496 | name = "httparse" 497 | version = "1.8.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 500 | 501 | [[package]] 502 | name = "hyper" 503 | version = "1.3.1" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" 506 | dependencies = [ 507 | "bytes", 508 | "futures-channel", 509 | "futures-util", 510 | "h2", 511 | "http", 512 | "http-body", 513 | "httparse", 514 | "itoa", 515 | "pin-project-lite", 516 | "smallvec", 517 | "tokio", 518 | "want", 519 | ] 520 | 521 | [[package]] 522 | name = "hyper-tls" 523 | version = "0.6.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 526 | dependencies = [ 527 | "bytes", 528 | "http-body-util", 529 | "hyper", 530 | "hyper-util", 531 | "native-tls", 532 | "tokio", 533 | "tokio-native-tls", 534 | "tower-service", 535 | ] 536 | 537 | [[package]] 538 | name = "hyper-util" 539 | version = "0.1.3" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" 542 | dependencies = [ 543 | "bytes", 544 | "futures-channel", 545 | "futures-util", 546 | "http", 547 | "http-body", 548 | "hyper", 549 | "pin-project-lite", 550 | "socket2", 551 | "tokio", 552 | "tower", 553 | "tower-service", 554 | "tracing", 555 | ] 556 | 557 | [[package]] 558 | name = "idna" 559 | version = "0.3.0" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 562 | dependencies = [ 563 | "unicode-bidi", 564 | "unicode-normalization", 565 | ] 566 | 567 | [[package]] 568 | name = "idna" 569 | version = "0.5.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 572 | dependencies = [ 573 | "unicode-bidi", 574 | "unicode-normalization", 575 | ] 576 | 577 | [[package]] 578 | name = "indexmap" 579 | version = "2.2.6" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 582 | dependencies = [ 583 | "equivalent", 584 | "hashbrown", 585 | ] 586 | 587 | [[package]] 588 | name = "indoc" 589 | version = "2.0.5" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 592 | 593 | [[package]] 594 | name = "ipnet" 595 | version = "2.9.0" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 598 | 599 | [[package]] 600 | name = "itoa" 601 | version = "1.0.11" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 604 | 605 | [[package]] 606 | name = "js-sys" 607 | version = "0.3.69" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" 610 | dependencies = [ 611 | "wasm-bindgen", 612 | ] 613 | 614 | [[package]] 615 | name = "jsonpath_lib" 616 | version = "0.3.0" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "eaa63191d68230cccb81c5aa23abd53ed64d83337cacbb25a7b8c7979523774f" 619 | dependencies = [ 620 | "log", 621 | "serde", 622 | "serde_json", 623 | ] 624 | 625 | [[package]] 626 | name = "lazy_static" 627 | version = "1.4.0" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 630 | 631 | [[package]] 632 | name = "libc" 633 | version = "0.2.155" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 636 | 637 | [[package]] 638 | name = "linux-raw-sys" 639 | version = "0.4.14" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 642 | 643 | [[package]] 644 | name = "lock_api" 645 | version = "0.4.12" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 648 | dependencies = [ 649 | "autocfg", 650 | "scopeguard", 651 | ] 652 | 653 | [[package]] 654 | name = "log" 655 | version = "0.4.21" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 658 | 659 | [[package]] 660 | name = "memchr" 661 | version = "2.7.2" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 664 | 665 | [[package]] 666 | name = "memoffset" 667 | version = "0.9.1" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 670 | dependencies = [ 671 | "autocfg", 672 | ] 673 | 674 | [[package]] 675 | name = "mime" 676 | version = "0.3.17" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 679 | 680 | [[package]] 681 | name = "mime_guess" 682 | version = "2.0.4" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 685 | dependencies = [ 686 | "mime", 687 | "unicase", 688 | ] 689 | 690 | [[package]] 691 | name = "miniz_oxide" 692 | version = "0.7.3" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" 695 | dependencies = [ 696 | "adler", 697 | ] 698 | 699 | [[package]] 700 | name = "mio" 701 | version = "0.8.11" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 704 | dependencies = [ 705 | "libc", 706 | "wasi", 707 | "windows-sys 0.48.0", 708 | ] 709 | 710 | [[package]] 711 | name = "native-tls" 712 | version = "0.2.11" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 715 | dependencies = [ 716 | "lazy_static", 717 | "libc", 718 | "log", 719 | "openssl", 720 | "openssl-probe", 721 | "openssl-sys", 722 | "schannel", 723 | "security-framework", 724 | "security-framework-sys", 725 | "tempfile", 726 | ] 727 | 728 | [[package]] 729 | name = "num-conv" 730 | version = "0.1.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 733 | 734 | [[package]] 735 | name = "num_cpus" 736 | version = "1.16.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 739 | dependencies = [ 740 | "hermit-abi", 741 | "libc", 742 | ] 743 | 744 | [[package]] 745 | name = "object" 746 | version = "0.32.2" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 749 | dependencies = [ 750 | "memchr", 751 | ] 752 | 753 | [[package]] 754 | name = "once_cell" 755 | version = "1.21.3" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 758 | 759 | [[package]] 760 | name = "openssl" 761 | version = "0.10.64" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" 764 | dependencies = [ 765 | "bitflags 2.5.0", 766 | "cfg-if", 767 | "foreign-types", 768 | "libc", 769 | "once_cell", 770 | "openssl-macros", 771 | "openssl-sys", 772 | ] 773 | 774 | [[package]] 775 | name = "openssl-macros" 776 | version = "0.1.1" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 779 | dependencies = [ 780 | "proc-macro2", 781 | "quote", 782 | "syn", 783 | ] 784 | 785 | [[package]] 786 | name = "openssl-probe" 787 | version = "0.1.5" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 790 | 791 | [[package]] 792 | name = "openssl-sys" 793 | version = "0.9.102" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" 796 | dependencies = [ 797 | "cc", 798 | "libc", 799 | "pkg-config", 800 | "vcpkg", 801 | ] 802 | 803 | [[package]] 804 | name = "os_info" 805 | version = "3.8.2" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" 808 | dependencies = [ 809 | "log", 810 | "serde", 811 | "windows-sys 0.52.0", 812 | ] 813 | 814 | [[package]] 815 | name = "parking_lot" 816 | version = "0.12.2" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" 819 | dependencies = [ 820 | "lock_api", 821 | "parking_lot_core", 822 | ] 823 | 824 | [[package]] 825 | name = "parking_lot_core" 826 | version = "0.9.10" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 829 | dependencies = [ 830 | "cfg-if", 831 | "libc", 832 | "redox_syscall", 833 | "smallvec", 834 | "windows-targets 0.52.5", 835 | ] 836 | 837 | [[package]] 838 | name = "percent-encoding" 839 | version = "2.3.1" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 842 | 843 | [[package]] 844 | name = "pest" 845 | version = "2.7.10" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" 848 | dependencies = [ 849 | "memchr", 850 | "thiserror", 851 | "ucd-trie", 852 | ] 853 | 854 | [[package]] 855 | name = "pest_derive" 856 | version = "2.7.10" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" 859 | dependencies = [ 860 | "pest", 861 | "pest_generator", 862 | ] 863 | 864 | [[package]] 865 | name = "pest_generator" 866 | version = "2.7.10" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" 869 | dependencies = [ 870 | "pest", 871 | "pest_meta", 872 | "proc-macro2", 873 | "quote", 874 | "syn", 875 | ] 876 | 877 | [[package]] 878 | name = "pest_meta" 879 | version = "2.7.10" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" 882 | dependencies = [ 883 | "once_cell", 884 | "pest", 885 | "sha2", 886 | ] 887 | 888 | [[package]] 889 | name = "pin-project" 890 | version = "1.1.5" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 893 | dependencies = [ 894 | "pin-project-internal", 895 | ] 896 | 897 | [[package]] 898 | name = "pin-project-internal" 899 | version = "1.1.5" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 902 | dependencies = [ 903 | "proc-macro2", 904 | "quote", 905 | "syn", 906 | ] 907 | 908 | [[package]] 909 | name = "pin-project-lite" 910 | version = "0.2.14" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 913 | 914 | [[package]] 915 | name = "pin-utils" 916 | version = "0.1.0" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 919 | 920 | [[package]] 921 | name = "pkg-config" 922 | version = "0.3.30" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 925 | 926 | [[package]] 927 | name = "portable-atomic" 928 | version = "1.6.0" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" 931 | 932 | [[package]] 933 | name = "powerfmt" 934 | version = "0.2.0" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 937 | 938 | [[package]] 939 | name = "ppv-lite86" 940 | version = "0.2.17" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 943 | 944 | [[package]] 945 | name = "proc-macro2" 946 | version = "1.0.83" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" 949 | dependencies = [ 950 | "unicode-ident", 951 | ] 952 | 953 | [[package]] 954 | name = "psl-types" 955 | version = "2.0.11" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" 958 | 959 | [[package]] 960 | name = "publicsuffix" 961 | version = "2.2.3" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" 964 | dependencies = [ 965 | "idna 0.3.0", 966 | "psl-types", 967 | ] 968 | 969 | [[package]] 970 | name = "pyo3" 971 | version = "0.27.1" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf" 974 | dependencies = [ 975 | "indoc", 976 | "libc", 977 | "memoffset", 978 | "once_cell", 979 | "portable-atomic", 980 | "pyo3-build-config", 981 | "pyo3-ffi", 982 | "pyo3-macros", 983 | "serde", 984 | "unindent", 985 | ] 986 | 987 | [[package]] 988 | name = "pyo3-async-runtimes" 989 | version = "0.27.0" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "57ddb5b570751e93cc6777e81fee8087e59cd53b5043292f2a6d59d5bd80fdfd" 992 | dependencies = [ 993 | "futures", 994 | "once_cell", 995 | "pin-project-lite", 996 | "pyo3", 997 | "pyo3-async-runtimes-macros", 998 | "tokio", 999 | ] 1000 | 1001 | [[package]] 1002 | name = "pyo3-async-runtimes-macros" 1003 | version = "0.27.0" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "bcd7d70ee0ca1661c40407e6f84e4463ef2658c90a9e2fbbd4515b2bcdfcaeca" 1006 | dependencies = [ 1007 | "proc-macro2", 1008 | "quote", 1009 | "syn", 1010 | ] 1011 | 1012 | [[package]] 1013 | name = "pyo3-build-config" 1014 | version = "0.27.1" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb" 1017 | dependencies = [ 1018 | "target-lexicon", 1019 | ] 1020 | 1021 | [[package]] 1022 | name = "pyo3-ffi" 1023 | version = "0.27.1" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be" 1026 | dependencies = [ 1027 | "libc", 1028 | "pyo3-build-config", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "pyo3-macros" 1033 | version = "0.27.1" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71" 1036 | dependencies = [ 1037 | "proc-macro2", 1038 | "pyo3-macros-backend", 1039 | "quote", 1040 | "syn", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "pyo3-macros-backend" 1045 | version = "0.27.1" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b" 1048 | dependencies = [ 1049 | "heck", 1050 | "proc-macro2", 1051 | "pyo3-build-config", 1052 | "quote", 1053 | "syn", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "quote" 1058 | version = "1.0.36" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 1061 | dependencies = [ 1062 | "proc-macro2", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "rand" 1067 | version = "0.8.5" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1070 | dependencies = [ 1071 | "libc", 1072 | "rand_chacha", 1073 | "rand_core", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "rand_chacha" 1078 | version = "0.3.1" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1081 | dependencies = [ 1082 | "ppv-lite86", 1083 | "rand_core", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "rand_core" 1088 | version = "0.6.4" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1091 | dependencies = [ 1092 | "getrandom", 1093 | ] 1094 | 1095 | [[package]] 1096 | name = "redox_syscall" 1097 | version = "0.5.1" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" 1100 | dependencies = [ 1101 | "bitflags 2.5.0", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "reqwest" 1106 | version = "0.12.4" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" 1109 | dependencies = [ 1110 | "base64", 1111 | "bytes", 1112 | "cookie", 1113 | "cookie_store", 1114 | "encoding_rs", 1115 | "futures-core", 1116 | "futures-util", 1117 | "h2", 1118 | "http", 1119 | "http-body", 1120 | "http-body-util", 1121 | "hyper", 1122 | "hyper-tls", 1123 | "hyper-util", 1124 | "ipnet", 1125 | "js-sys", 1126 | "log", 1127 | "mime", 1128 | "mime_guess", 1129 | "native-tls", 1130 | "once_cell", 1131 | "percent-encoding", 1132 | "pin-project-lite", 1133 | "rustls-pemfile", 1134 | "serde", 1135 | "serde_json", 1136 | "serde_urlencoded", 1137 | "sync_wrapper", 1138 | "system-configuration", 1139 | "tokio", 1140 | "tokio-native-tls", 1141 | "tokio-util", 1142 | "tower-service", 1143 | "url", 1144 | "wasm-bindgen", 1145 | "wasm-bindgen-futures", 1146 | "wasm-streams", 1147 | "web-sys", 1148 | "winreg", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "rustc-demangle" 1153 | version = "0.1.24" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1156 | 1157 | [[package]] 1158 | name = "rustix" 1159 | version = "0.38.34" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 1162 | dependencies = [ 1163 | "bitflags 2.5.0", 1164 | "errno", 1165 | "libc", 1166 | "linux-raw-sys", 1167 | "windows-sys 0.52.0", 1168 | ] 1169 | 1170 | [[package]] 1171 | name = "rustls-pemfile" 1172 | version = "2.1.2" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" 1175 | dependencies = [ 1176 | "base64", 1177 | "rustls-pki-types", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "rustls-pki-types" 1182 | version = "1.7.0" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" 1185 | 1186 | [[package]] 1187 | name = "ryu" 1188 | version = "1.0.18" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1191 | 1192 | [[package]] 1193 | name = "schannel" 1194 | version = "0.1.23" 1195 | source = "registry+https://github.com/rust-lang/crates.io-index" 1196 | checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" 1197 | dependencies = [ 1198 | "windows-sys 0.52.0", 1199 | ] 1200 | 1201 | [[package]] 1202 | name = "scopeguard" 1203 | version = "1.2.0" 1204 | source = "registry+https://github.com/rust-lang/crates.io-index" 1205 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1206 | 1207 | [[package]] 1208 | name = "security-framework" 1209 | version = "2.11.0" 1210 | source = "registry+https://github.com/rust-lang/crates.io-index" 1211 | checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" 1212 | dependencies = [ 1213 | "bitflags 2.5.0", 1214 | "core-foundation", 1215 | "core-foundation-sys", 1216 | "libc", 1217 | "security-framework-sys", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "security-framework-sys" 1222 | version = "2.11.0" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" 1225 | dependencies = [ 1226 | "core-foundation-sys", 1227 | "libc", 1228 | ] 1229 | 1230 | [[package]] 1231 | name = "serde" 1232 | version = "1.0.202" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" 1235 | dependencies = [ 1236 | "serde_derive", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "serde_derive" 1241 | version = "1.0.202" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" 1244 | dependencies = [ 1245 | "proc-macro2", 1246 | "quote", 1247 | "syn", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "serde_json" 1252 | version = "1.0.117" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" 1255 | dependencies = [ 1256 | "indexmap", 1257 | "itoa", 1258 | "ryu", 1259 | "serde", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "serde_urlencoded" 1264 | version = "0.7.1" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1267 | dependencies = [ 1268 | "form_urlencoded", 1269 | "itoa", 1270 | "ryu", 1271 | "serde", 1272 | ] 1273 | 1274 | [[package]] 1275 | name = "sha2" 1276 | version = "0.10.8" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1279 | dependencies = [ 1280 | "cfg-if", 1281 | "cpufeatures", 1282 | "digest", 1283 | ] 1284 | 1285 | [[package]] 1286 | name = "signal-hook-registry" 1287 | version = "1.4.2" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1290 | dependencies = [ 1291 | "libc", 1292 | ] 1293 | 1294 | [[package]] 1295 | name = "slab" 1296 | version = "0.4.9" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1299 | dependencies = [ 1300 | "autocfg", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "smallvec" 1305 | version = "1.13.2" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1308 | 1309 | [[package]] 1310 | name = "socket2" 1311 | version = "0.5.7" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1314 | dependencies = [ 1315 | "libc", 1316 | "windows-sys 0.52.0", 1317 | ] 1318 | 1319 | [[package]] 1320 | name = "syn" 1321 | version = "2.0.65" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" 1324 | dependencies = [ 1325 | "proc-macro2", 1326 | "quote", 1327 | "unicode-ident", 1328 | ] 1329 | 1330 | [[package]] 1331 | name = "sync_wrapper" 1332 | version = "0.1.2" 1333 | source = "registry+https://github.com/rust-lang/crates.io-index" 1334 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1335 | 1336 | [[package]] 1337 | name = "system-configuration" 1338 | version = "0.5.1" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 1341 | dependencies = [ 1342 | "bitflags 1.3.2", 1343 | "core-foundation", 1344 | "system-configuration-sys", 1345 | ] 1346 | 1347 | [[package]] 1348 | name = "system-configuration-sys" 1349 | version = "0.5.0" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 1352 | dependencies = [ 1353 | "core-foundation-sys", 1354 | "libc", 1355 | ] 1356 | 1357 | [[package]] 1358 | name = "target-lexicon" 1359 | version = "0.13.3" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" 1362 | 1363 | [[package]] 1364 | name = "tempfile" 1365 | version = "3.10.1" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" 1368 | dependencies = [ 1369 | "cfg-if", 1370 | "fastrand", 1371 | "rustix", 1372 | "windows-sys 0.52.0", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "thiserror" 1377 | version = "1.0.61" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" 1380 | dependencies = [ 1381 | "thiserror-impl", 1382 | ] 1383 | 1384 | [[package]] 1385 | name = "thiserror-impl" 1386 | version = "1.0.61" 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" 1388 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" 1389 | dependencies = [ 1390 | "proc-macro2", 1391 | "quote", 1392 | "syn", 1393 | ] 1394 | 1395 | [[package]] 1396 | name = "time" 1397 | version = "0.3.36" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1400 | dependencies = [ 1401 | "deranged", 1402 | "itoa", 1403 | "num-conv", 1404 | "powerfmt", 1405 | "serde", 1406 | "time-core", 1407 | "time-macros", 1408 | ] 1409 | 1410 | [[package]] 1411 | name = "time-core" 1412 | version = "0.1.2" 1413 | source = "registry+https://github.com/rust-lang/crates.io-index" 1414 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1415 | 1416 | [[package]] 1417 | name = "time-macros" 1418 | version = "0.2.18" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1421 | dependencies = [ 1422 | "num-conv", 1423 | "time-core", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "tinyvec" 1428 | version = "1.6.0" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1431 | dependencies = [ 1432 | "tinyvec_macros", 1433 | ] 1434 | 1435 | [[package]] 1436 | name = "tinyvec_macros" 1437 | version = "0.1.1" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1440 | 1441 | [[package]] 1442 | name = "tokio" 1443 | version = "1.37.0" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" 1446 | dependencies = [ 1447 | "backtrace", 1448 | "bytes", 1449 | "libc", 1450 | "mio", 1451 | "num_cpus", 1452 | "parking_lot", 1453 | "pin-project-lite", 1454 | "signal-hook-registry", 1455 | "socket2", 1456 | "tokio-macros", 1457 | "windows-sys 0.48.0", 1458 | ] 1459 | 1460 | [[package]] 1461 | name = "tokio-macros" 1462 | version = "2.2.0" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 1465 | dependencies = [ 1466 | "proc-macro2", 1467 | "quote", 1468 | "syn", 1469 | ] 1470 | 1471 | [[package]] 1472 | name = "tokio-native-tls" 1473 | version = "0.3.1" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1476 | dependencies = [ 1477 | "native-tls", 1478 | "tokio", 1479 | ] 1480 | 1481 | [[package]] 1482 | name = "tokio-util" 1483 | version = "0.7.11" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" 1486 | dependencies = [ 1487 | "bytes", 1488 | "futures-core", 1489 | "futures-sink", 1490 | "pin-project-lite", 1491 | "tokio", 1492 | ] 1493 | 1494 | [[package]] 1495 | name = "tower" 1496 | version = "0.4.13" 1497 | source = "registry+https://github.com/rust-lang/crates.io-index" 1498 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1499 | dependencies = [ 1500 | "futures-core", 1501 | "futures-util", 1502 | "pin-project", 1503 | "pin-project-lite", 1504 | "tokio", 1505 | "tower-layer", 1506 | "tower-service", 1507 | "tracing", 1508 | ] 1509 | 1510 | [[package]] 1511 | name = "tower-layer" 1512 | version = "0.3.2" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" 1515 | 1516 | [[package]] 1517 | name = "tower-service" 1518 | version = "0.3.2" 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" 1520 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1521 | 1522 | [[package]] 1523 | name = "tracing" 1524 | version = "0.1.40" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1527 | dependencies = [ 1528 | "log", 1529 | "pin-project-lite", 1530 | "tracing-core", 1531 | ] 1532 | 1533 | [[package]] 1534 | name = "tracing-core" 1535 | version = "0.1.32" 1536 | source = "registry+https://github.com/rust-lang/crates.io-index" 1537 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1538 | dependencies = [ 1539 | "once_cell", 1540 | ] 1541 | 1542 | [[package]] 1543 | name = "try-lock" 1544 | version = "0.2.5" 1545 | source = "registry+https://github.com/rust-lang/crates.io-index" 1546 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1547 | 1548 | [[package]] 1549 | name = "typenum" 1550 | version = "1.17.0" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1553 | 1554 | [[package]] 1555 | name = "ucd-trie" 1556 | version = "0.1.6" 1557 | source = "registry+https://github.com/rust-lang/crates.io-index" 1558 | checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" 1559 | 1560 | [[package]] 1561 | name = "unicase" 1562 | version = "2.7.0" 1563 | source = "registry+https://github.com/rust-lang/crates.io-index" 1564 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 1565 | dependencies = [ 1566 | "version_check", 1567 | ] 1568 | 1569 | [[package]] 1570 | name = "unicode-bidi" 1571 | version = "0.3.15" 1572 | source = "registry+https://github.com/rust-lang/crates.io-index" 1573 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1574 | 1575 | [[package]] 1576 | name = "unicode-ident" 1577 | version = "1.0.12" 1578 | source = "registry+https://github.com/rust-lang/crates.io-index" 1579 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1580 | 1581 | [[package]] 1582 | name = "unicode-normalization" 1583 | version = "0.1.23" 1584 | source = "registry+https://github.com/rust-lang/crates.io-index" 1585 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1586 | dependencies = [ 1587 | "tinyvec", 1588 | ] 1589 | 1590 | [[package]] 1591 | name = "unindent" 1592 | version = "0.2.3" 1593 | source = "registry+https://github.com/rust-lang/crates.io-index" 1594 | checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" 1595 | 1596 | [[package]] 1597 | name = "url" 1598 | version = "2.5.0" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 1601 | dependencies = [ 1602 | "form_urlencoded", 1603 | "idna 0.5.0", 1604 | "percent-encoding", 1605 | ] 1606 | 1607 | [[package]] 1608 | name = "vcpkg" 1609 | version = "0.2.15" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1612 | 1613 | [[package]] 1614 | name = "version_check" 1615 | version = "0.9.4" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1618 | 1619 | [[package]] 1620 | name = "want" 1621 | version = "0.3.1" 1622 | source = "registry+https://github.com/rust-lang/crates.io-index" 1623 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1624 | dependencies = [ 1625 | "try-lock", 1626 | ] 1627 | 1628 | [[package]] 1629 | name = "wasi" 1630 | version = "0.11.0+wasi-snapshot-preview1" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1633 | 1634 | [[package]] 1635 | name = "wasm-bindgen" 1636 | version = "0.2.92" 1637 | source = "registry+https://github.com/rust-lang/crates.io-index" 1638 | checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" 1639 | dependencies = [ 1640 | "cfg-if", 1641 | "wasm-bindgen-macro", 1642 | ] 1643 | 1644 | [[package]] 1645 | name = "wasm-bindgen-backend" 1646 | version = "0.2.92" 1647 | source = "registry+https://github.com/rust-lang/crates.io-index" 1648 | checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" 1649 | dependencies = [ 1650 | "bumpalo", 1651 | "log", 1652 | "once_cell", 1653 | "proc-macro2", 1654 | "quote", 1655 | "syn", 1656 | "wasm-bindgen-shared", 1657 | ] 1658 | 1659 | [[package]] 1660 | name = "wasm-bindgen-futures" 1661 | version = "0.4.42" 1662 | source = "registry+https://github.com/rust-lang/crates.io-index" 1663 | checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" 1664 | dependencies = [ 1665 | "cfg-if", 1666 | "js-sys", 1667 | "wasm-bindgen", 1668 | "web-sys", 1669 | ] 1670 | 1671 | [[package]] 1672 | name = "wasm-bindgen-macro" 1673 | version = "0.2.92" 1674 | source = "registry+https://github.com/rust-lang/crates.io-index" 1675 | checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" 1676 | dependencies = [ 1677 | "quote", 1678 | "wasm-bindgen-macro-support", 1679 | ] 1680 | 1681 | [[package]] 1682 | name = "wasm-bindgen-macro-support" 1683 | version = "0.2.92" 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" 1685 | checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" 1686 | dependencies = [ 1687 | "proc-macro2", 1688 | "quote", 1689 | "syn", 1690 | "wasm-bindgen-backend", 1691 | "wasm-bindgen-shared", 1692 | ] 1693 | 1694 | [[package]] 1695 | name = "wasm-bindgen-shared" 1696 | version = "0.2.92" 1697 | source = "registry+https://github.com/rust-lang/crates.io-index" 1698 | checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" 1699 | 1700 | [[package]] 1701 | name = "wasm-streams" 1702 | version = "0.4.0" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" 1705 | dependencies = [ 1706 | "futures-util", 1707 | "js-sys", 1708 | "wasm-bindgen", 1709 | "wasm-bindgen-futures", 1710 | "web-sys", 1711 | ] 1712 | 1713 | [[package]] 1714 | name = "web-sys" 1715 | version = "0.3.69" 1716 | source = "registry+https://github.com/rust-lang/crates.io-index" 1717 | checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" 1718 | dependencies = [ 1719 | "js-sys", 1720 | "wasm-bindgen", 1721 | ] 1722 | 1723 | [[package]] 1724 | name = "winapi" 1725 | version = "0.3.9" 1726 | source = "registry+https://github.com/rust-lang/crates.io-index" 1727 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1728 | dependencies = [ 1729 | "winapi-i686-pc-windows-gnu", 1730 | "winapi-x86_64-pc-windows-gnu", 1731 | ] 1732 | 1733 | [[package]] 1734 | name = "winapi-i686-pc-windows-gnu" 1735 | version = "0.4.0" 1736 | source = "registry+https://github.com/rust-lang/crates.io-index" 1737 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1738 | 1739 | [[package]] 1740 | name = "winapi-x86_64-pc-windows-gnu" 1741 | version = "0.4.0" 1742 | source = "registry+https://github.com/rust-lang/crates.io-index" 1743 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1744 | 1745 | [[package]] 1746 | name = "windows-sys" 1747 | version = "0.48.0" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1750 | dependencies = [ 1751 | "windows-targets 0.48.5", 1752 | ] 1753 | 1754 | [[package]] 1755 | name = "windows-sys" 1756 | version = "0.52.0" 1757 | source = "registry+https://github.com/rust-lang/crates.io-index" 1758 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1759 | dependencies = [ 1760 | "windows-targets 0.52.5", 1761 | ] 1762 | 1763 | [[package]] 1764 | name = "windows-targets" 1765 | version = "0.48.5" 1766 | source = "registry+https://github.com/rust-lang/crates.io-index" 1767 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1768 | dependencies = [ 1769 | "windows_aarch64_gnullvm 0.48.5", 1770 | "windows_aarch64_msvc 0.48.5", 1771 | "windows_i686_gnu 0.48.5", 1772 | "windows_i686_msvc 0.48.5", 1773 | "windows_x86_64_gnu 0.48.5", 1774 | "windows_x86_64_gnullvm 0.48.5", 1775 | "windows_x86_64_msvc 0.48.5", 1776 | ] 1777 | 1778 | [[package]] 1779 | name = "windows-targets" 1780 | version = "0.52.5" 1781 | source = "registry+https://github.com/rust-lang/crates.io-index" 1782 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1783 | dependencies = [ 1784 | "windows_aarch64_gnullvm 0.52.5", 1785 | "windows_aarch64_msvc 0.52.5", 1786 | "windows_i686_gnu 0.52.5", 1787 | "windows_i686_gnullvm", 1788 | "windows_i686_msvc 0.52.5", 1789 | "windows_x86_64_gnu 0.52.5", 1790 | "windows_x86_64_gnullvm 0.52.5", 1791 | "windows_x86_64_msvc 0.52.5", 1792 | ] 1793 | 1794 | [[package]] 1795 | name = "windows_aarch64_gnullvm" 1796 | version = "0.48.5" 1797 | source = "registry+https://github.com/rust-lang/crates.io-index" 1798 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1799 | 1800 | [[package]] 1801 | name = "windows_aarch64_gnullvm" 1802 | version = "0.52.5" 1803 | source = "registry+https://github.com/rust-lang/crates.io-index" 1804 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1805 | 1806 | [[package]] 1807 | name = "windows_aarch64_msvc" 1808 | version = "0.48.5" 1809 | source = "registry+https://github.com/rust-lang/crates.io-index" 1810 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1811 | 1812 | [[package]] 1813 | name = "windows_aarch64_msvc" 1814 | version = "0.52.5" 1815 | source = "registry+https://github.com/rust-lang/crates.io-index" 1816 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1817 | 1818 | [[package]] 1819 | name = "windows_i686_gnu" 1820 | version = "0.48.5" 1821 | source = "registry+https://github.com/rust-lang/crates.io-index" 1822 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1823 | 1824 | [[package]] 1825 | name = "windows_i686_gnu" 1826 | version = "0.52.5" 1827 | source = "registry+https://github.com/rust-lang/crates.io-index" 1828 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1829 | 1830 | [[package]] 1831 | name = "windows_i686_gnullvm" 1832 | version = "0.52.5" 1833 | source = "registry+https://github.com/rust-lang/crates.io-index" 1834 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1835 | 1836 | [[package]] 1837 | name = "windows_i686_msvc" 1838 | version = "0.48.5" 1839 | source = "registry+https://github.com/rust-lang/crates.io-index" 1840 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1841 | 1842 | [[package]] 1843 | name = "windows_i686_msvc" 1844 | version = "0.52.5" 1845 | source = "registry+https://github.com/rust-lang/crates.io-index" 1846 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1847 | 1848 | [[package]] 1849 | name = "windows_x86_64_gnu" 1850 | version = "0.48.5" 1851 | source = "registry+https://github.com/rust-lang/crates.io-index" 1852 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1853 | 1854 | [[package]] 1855 | name = "windows_x86_64_gnu" 1856 | version = "0.52.5" 1857 | source = "registry+https://github.com/rust-lang/crates.io-index" 1858 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1859 | 1860 | [[package]] 1861 | name = "windows_x86_64_gnullvm" 1862 | version = "0.48.5" 1863 | source = "registry+https://github.com/rust-lang/crates.io-index" 1864 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1865 | 1866 | [[package]] 1867 | name = "windows_x86_64_gnullvm" 1868 | version = "0.52.5" 1869 | source = "registry+https://github.com/rust-lang/crates.io-index" 1870 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1871 | 1872 | [[package]] 1873 | name = "windows_x86_64_msvc" 1874 | version = "0.48.5" 1875 | source = "registry+https://github.com/rust-lang/crates.io-index" 1876 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1877 | 1878 | [[package]] 1879 | name = "windows_x86_64_msvc" 1880 | version = "0.52.5" 1881 | source = "registry+https://github.com/rust-lang/crates.io-index" 1882 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1883 | 1884 | [[package]] 1885 | name = "winreg" 1886 | version = "0.52.0" 1887 | source = "registry+https://github.com/rust-lang/crates.io-index" 1888 | checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" 1889 | dependencies = [ 1890 | "cfg-if", 1891 | "windows-sys 0.48.0", 1892 | ] 1893 | --------------------------------------------------------------------------------