├── primp ├── py.typed ├── primp.pyi └── __init__.py ├── benchmark.jpg ├── benchmark ├── requirements.txt ├── README.md ├── server.py ├── generate_image.py └── benchmark.py ├── tests ├── test_response.py ├── test_asyncclient.py ├── test_client.py └── test_defs.py ├── .gitignore ├── LICENSE ├── Cargo.toml ├── pyproject.toml ├── src ├── traits.rs ├── utils.rs ├── impersonate.rs ├── response.rs └── lib.rs ├── README.md ├── .github └── workflows │ └── CI.yml └── Cargo.lock /primp/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /benchmark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deedy5/primp/HEAD/benchmark.jpg -------------------------------------------------------------------------------- /benchmark/requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib 2 | pandas 3 | starlette 4 | uvicorn 5 | aiohttp 6 | requests 7 | httpx 8 | tls-client 9 | primp 10 | curl_cffi 11 | pycurl 12 | typing_extensions # tls-client py3.12 dependence 13 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | ## Benchmark 2 | 3 | Benchmark between `primp` and other python http clients: 4 | 5 | - curl_cffi 6 | - httpx 7 | - primp 8 | - pycurl 9 | - python-tls-client 10 | - requests 11 | 12 | Server response is gzipped. 13 | 14 | #### Run benchmark: 15 | 16 | - run server: `uvicorn server:app` 17 | - run benchmark: `python benchmark.py` 18 | -------------------------------------------------------------------------------- /tests/test_response.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import pytest 4 | 5 | import certifi 6 | import primp 7 | 8 | 9 | def retry(max_retries=3, delay=1): 10 | def decorator(func): 11 | def wrapper(*args, **kwargs): 12 | for attempt in range(max_retries): 13 | try: 14 | return func(*args, **kwargs) 15 | except Exception as e: 16 | if attempt < max_retries - 1: 17 | sleep(delay) 18 | continue 19 | else: 20 | raise e 21 | 22 | return wrapper 23 | 24 | return decorator 25 | 26 | 27 | @retry() 28 | def test_response_stream(): 29 | client = primp.Client(impersonate="chrome_133", impersonate_os="windows") 30 | resp = client.get("https://nytimes.com") 31 | for chunk in resp.stream(): 32 | assert len(chunk) > 0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | .pytest_cache/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | .venv/ 14 | env/ 15 | bin/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | include/ 26 | man/ 27 | venv/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | pip-selfcheck.json 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | 60 | .DS_Store 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyCharm 66 | .idea/ 67 | 68 | # VSCode 69 | .vscode/ 70 | 71 | # Pyenv 72 | .python-version 73 | 74 | # Ignore csv and jpg files in benchmark folder 75 | benchmark/*.csv 76 | benchmark/*.jpg 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 deedy5 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 | -------------------------------------------------------------------------------- /benchmark/server.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import gzip 4 | from starlette.applications import Starlette 5 | from starlette.responses import Response 6 | from starlette.routing import Route 7 | 8 | random_5k = base64.b64encode(os.urandom(5 * 1024)).decode('utf-8') 9 | random_5k = gzip.compress(random_5k.encode('utf-8')) 10 | 11 | random_50k = base64.b64encode(os.urandom(50 * 1024)).decode('utf-8') 12 | random_50k = gzip.compress(random_50k.encode('utf-8')) 13 | 14 | random_200k = base64.b64encode(os.urandom(200 * 1024)).decode('utf-8') 15 | random_200k = gzip.compress(random_200k.encode('utf-8')) 16 | 17 | 18 | def gzip_response(gzipped_content): 19 | headers = { 20 | 'Content-Encoding': 'gzip', 21 | 'Content-Length': str(len(gzipped_content)), 22 | } 23 | return Response(gzipped_content, headers=headers) 24 | 25 | app = Starlette( 26 | routes=[ 27 | Route("/5k", lambda r: gzip_response(random_5k)), 28 | Route("/50k", lambda r: gzip_response(random_50k)), 29 | Route("/200k", lambda r: gzip_response(random_200k)), 30 | ], 31 | ) 32 | 33 | # Run server: uvicorn server:app 34 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "primp" 3 | version = "0.15.0" 4 | edition = "2021" 5 | description = "HTTP client that can impersonate web browsers, mimicking their headers and `TLS/JA3/JA4/HTTP2` fingerprints" 6 | authors = ["deedy5"] 7 | 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | [lib] 11 | name = "primp" 12 | crate-type = ["cdylib"] 13 | 14 | [dependencies] 15 | pyo3 = { version = "0.24.1", features = ["extension-module", "abi3-py38", "indexmap", "anyhow", "multiple-pymethods"] } 16 | anyhow = "1.0.98" 17 | tracing = { version = "0.1.41", features = ["log-always"] } 18 | pyo3-log = "0.12.3" 19 | rquest = { version = "2.1.6", features = [ 20 | "json", 21 | "cookies", 22 | "socks", 23 | "gzip", 24 | "brotli", 25 | "zstd", 26 | "deflate", 27 | "multipart", 28 | "stream", 29 | ] } 30 | encoding_rs = { version = "0.8.35" } 31 | foldhash = "0.1.5" 32 | indexmap = { version = "2.9.0", features = ["serde"] } 33 | tokio = { version = "1.44.2", features = ["full"] } 34 | tokio-util = { version = "0.7.14", features = ["codec"] } # for multipart 35 | html2text = "0.14.3" 36 | pythonize = "0.24.0" 37 | serde_json = "1.0.140" 38 | webpki-root-certs = "0.26.8" 39 | http-body-util = "0.1.3" 40 | http = "1.3.1" 41 | mime = "0.3.17" 42 | rand = "0.9.0" 43 | 44 | [profile.release] 45 | codegen-units = 1 46 | lto = "fat" 47 | opt-level = 3 48 | panic = "abort" 49 | strip = "symbols" 50 | -------------------------------------------------------------------------------- /tests/test_asyncclient.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytest 3 | 4 | import certifi 5 | import primp 6 | 7 | 8 | def async_retry(max_retries=3, delay=1): 9 | def decorator(func): 10 | async def wrapper(*args, **kwargs): 11 | for attempt in range(max_retries): 12 | try: 13 | return await func(*args, **kwargs) 14 | except Exception as e: 15 | if attempt < max_retries - 1: 16 | await asyncio.sleep(delay) 17 | continue 18 | else: 19 | raise e 20 | 21 | return wrapper 22 | 23 | return decorator 24 | 25 | 26 | @pytest.mark.asyncio 27 | @async_retry() 28 | async def test_asyncclient_init(): 29 | auth = ("user", "password") 30 | headers = {"X-Test": "test"} 31 | cookies = {"ccc": "ddd", "cccc": "dddd"} 32 | params = {"x": "aaa", "y": "bbb"} 33 | client = primp.AsyncClient( 34 | auth=auth, 35 | params=params, 36 | headers=headers, 37 | ca_cert_file=certifi.where(), 38 | ) 39 | client.set_cookies("https://httpbin.org", cookies) 40 | response = await client.get("https://httpbin.org/anything") 41 | assert response.status_code == 200 42 | json_data = response.json() 43 | assert json_data["headers"]["X-Test"] == "test" 44 | assert json_data["headers"]["Authorization"] == "Basic dXNlcjpwYXNzd29yZA==" 45 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 46 | # temp 47 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 48 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 49 | assert "cccc=dddd" in json_data["headers"]["Cookie"] -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.5,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "primp" 7 | description = "HTTP client that can impersonate web browsers, mimicking their headers and `TLS/JA3/JA4/HTTP2` fingerprints" 8 | requires-python = ">=3.8" 9 | license = {text = "MIT License"} 10 | keywords = ["python", "request", "impersonate"] 11 | authors = [ 12 | {name = "deedy5"} 13 | ] 14 | classifiers = [ 15 | "Programming Language :: Rust", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3 :: Only", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | "Programming Language :: Python :: Implementation :: CPython", 25 | "Programming Language :: Python :: Implementation :: PyPy", 26 | "Topic :: Internet :: WWW/HTTP", 27 | "Topic :: Software Development :: Libraries :: Python Modules", 28 | ] 29 | dynamic = ["version"] 30 | 31 | dependencies = [] 32 | 33 | [project.optional-dependencies] 34 | dev = [ 35 | "certifi", 36 | "pytest>=8.1.1", 37 | "pytest-asyncio>=0.25.3", 38 | "typing_extensions; python_version <= '3.11'", # for Unpack[TypedDict] 39 | "mypy>=1.14.1", 40 | "ruff>=0.9.2" 41 | ] 42 | 43 | [tool.maturin] 44 | features = ["pyo3/extension-module"] 45 | 46 | [tool.ruff] 47 | line-length = 120 48 | exclude = ["tests"] 49 | 50 | [tool.ruff.lint] 51 | select = [ 52 | "E", # pycodestyle 53 | "F", # Pyflakes 54 | "UP", # pyupgrade 55 | "B", # flake8-bugbear 56 | "SIM", # flake8-simplify 57 | "I", # isort 58 | ] 59 | 60 | [tool.mypy] 61 | python_version = "3.8" -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Error, Ok, Result}; 2 | use foldhash::fast::RandomState; 3 | use indexmap::IndexMap; 4 | 5 | use rquest::header::{HeaderMap, HeaderName, HeaderValue}; 6 | 7 | type IndexMapSSR = IndexMap; 8 | 9 | pub trait HeadersTraits { 10 | fn to_indexmap(&self) -> IndexMapSSR; 11 | fn to_headermap(&self) -> HeaderMap; 12 | fn insert_key_value(&mut self, key: String, value: String) -> Result<(), Error>; 13 | } 14 | 15 | impl HeadersTraits for IndexMapSSR { 16 | fn to_indexmap(&self) -> IndexMapSSR { 17 | self.clone() 18 | } 19 | fn to_headermap(&self) -> HeaderMap { 20 | let mut header_map = HeaderMap::with_capacity(self.len()); 21 | for (k, v) in self { 22 | header_map.insert( 23 | HeaderName::from_bytes(k.as_bytes()) 24 | .unwrap_or_else(|k| panic!("Invalid header name: {k:?}")), 25 | HeaderValue::from_bytes(v.as_bytes()) 26 | .unwrap_or_else(|v| panic!("Invalid header value: {v:?}")), 27 | ); 28 | } 29 | header_map 30 | } 31 | 32 | fn insert_key_value(&mut self, key: String, value: String) -> Result<(), Error> { 33 | self.insert(key.to_string(), value.to_string()); 34 | Ok(()) 35 | } 36 | } 37 | 38 | impl HeadersTraits for HeaderMap { 39 | fn to_indexmap(&self) -> IndexMapSSR { 40 | let mut index_map = 41 | IndexMapSSR::with_capacity_and_hasher(self.len(), RandomState::default()); 42 | for (key, value) in self { 43 | index_map.insert( 44 | key.as_str().to_string(), 45 | value 46 | .to_str() 47 | .unwrap_or_else(|v| panic!("Invalid header value: {v:?}")) 48 | .to_string(), 49 | ); 50 | } 51 | index_map 52 | } 53 | 54 | fn to_headermap(&self) -> HeaderMap { 55 | self.clone() 56 | } 57 | 58 | fn insert_key_value(&mut self, key: String, value: String) -> Result<(), Error> { 59 | let header_name = HeaderName::from_bytes(key.as_bytes()) 60 | .unwrap_or_else(|k| panic!("Invalid header name: {k:?}")); 61 | let header_value = HeaderValue::from_bytes(value.as_bytes()) 62 | .unwrap_or_else(|k| panic!("Invalid header value: {k:?}")); 63 | self.insert(header_name, header_value); 64 | Ok(()) 65 | } 66 | } 67 | 68 | pub trait CookiesTraits { 69 | fn to_string(&self) -> String; 70 | } 71 | 72 | impl CookiesTraits for IndexMapSSR { 73 | fn to_string(&self) -> String { 74 | let mut result = String::with_capacity(self.len() * 40); 75 | for (k, v) in self { 76 | if !result.is_empty() { 77 | result.push_str("; "); 78 | } 79 | result.push_str(k); 80 | result.push('='); 81 | result.push_str(v); 82 | } 83 | result 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use rquest::{X509Store, X509StoreBuilder, X509}; 4 | use tracing; 5 | 6 | /// Loads the CA certificates from venv var PRIMP_CA_BUNDLE or the WebPKI certificate store 7 | pub fn load_ca_certs() -> Option<&'static X509Store> { 8 | static CERT_STORE: LazyLock> = LazyLock::new(|| { 9 | let mut ca_store = X509StoreBuilder::new()?; 10 | if let Ok(ca_cert_path) = std::env::var("PRIMP_CA_BUNDLE").or(std::env::var("CA_CERT_FILE")) 11 | { 12 | // Use CA certificate bundle from env var PRIMP_CA_BUNDLE 13 | let cert_file = &std::fs::read(ca_cert_path) 14 | .expect("Failed to read file from env var PRIMP_CA_BUNDLE"); 15 | let certs = X509::stack_from_pem(cert_file)?; 16 | for cert in certs { 17 | ca_store.add_cert(cert)?; 18 | } 19 | } else { 20 | // Use WebPKI certificate store (Mozilla's trusted root certificates) 21 | for cert in webpki_root_certs::TLS_SERVER_ROOT_CERTS { 22 | let x509 = X509::from_der(cert)?; 23 | ca_store.add_cert(x509)?; 24 | } 25 | } 26 | Ok(ca_store.build()) 27 | }); 28 | 29 | match CERT_STORE.as_ref() { 30 | Ok(cert_store) => { 31 | tracing::debug!("Loaded CA certs"); 32 | Some(cert_store) 33 | } 34 | Err(err) => { 35 | tracing::error!("Failed to load CA certs: {:?}", err); 36 | None 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod load_ca_certs_tests { 43 | use super::*; 44 | use std::env; 45 | use std::fs; 46 | use std::path::Path; 47 | 48 | #[test] 49 | fn test_load_ca_certs_with_env_var() { 50 | // Create a temporary file with a CA certificate 51 | let ca_cert_path = Path::new("test_ca_cert.pem"); 52 | let ca_cert = "-----BEGIN CERTIFICATE----- 53 | MIIDdTCCAl2gAwIBAgIVAMIIujU9wQIBADANBgkqhkiG9w0BAQUFADBGMQswCQYD 54 | VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4g 55 | Q29sbGVjdGlvbjEgMB4GA1UECgwXUG9zdGdyZXMgQ29uc3VsdGF0aW9uczEhMB8G 56 | A1UECwwYUG9zdGdyZXMgQ29uc3VsdGF0aW9uczEhMB8GA1UEAwwYUG9zdGdyZXMg 57 | Q29uc3VsdGF0aW9uczEiMCAGCSqGSIb3DQEJARYTcGVyc29uYWwtZW1haWwuY29t 58 | MIIDdTCCAl2gAwIBAgIVAMIIujU9wQIBADANBgkqhkiG9w0BAQUFADBGMQswCQYD 59 | VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4g 60 | Q29sbGVjdGlvbjEgMB4GA1UECgwXUG9zdGdyZXMgQ29uc3VsdGF0aW9uczEhMB8G 61 | A1UECwwYUG9zdGdyZXMgQ29uc3VsdGF0aW9uczEhMB8GA1UEAwwYUG9zdGdyZXMg 62 | Q29uc3VsdGF0aW9uczEiMCAGCSqGSIb3DQEJARYTcGVyc29uYWwtZW1haWwuY29t 63 | -----END CERTIFICATE-----"; 64 | fs::write(ca_cert_path, ca_cert).unwrap(); 65 | 66 | // Set the environment variable 67 | env::set_var("PRIMP_CA_BUNDLE", ca_cert_path); 68 | 69 | // Call the function 70 | let result = load_ca_certs(); 71 | 72 | // Check the result 73 | assert!(result.is_some()); 74 | 75 | // Clean up 76 | fs::remove_file(ca_cert_path).unwrap(); 77 | } 78 | 79 | #[test] 80 | fn test_load_ca_certs_without_env_var() { 81 | // Call the function 82 | let result = load_ca_certs(); 83 | 84 | // Check the result 85 | assert!(result.is_some()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /benchmark/generate_image.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | import csv 4 | 5 | 6 | # Function to read and plot data from a CSV file 7 | def plot_data(file_name, ax, offset): 8 | with open(file_name, "r") as file: 9 | reader = csv.reader(file) 10 | next(reader) # Skip the header row 11 | data = list(reader) 12 | 13 | # Extract the data 14 | names = [row[0] for row in data] 15 | time_5k = [float(row[7]) for row in data] 16 | time_50k = [float(row[6]) for row in data] 17 | time_200k = [float(row[5]) for row in data] 18 | cpu_time_5k = [float(row[4]) for row in data] 19 | cpu_time_50k = [float(row[3]) for row in data] 20 | cpu_time_200k = [float(row[2]) for row in data] 21 | 22 | # Prepare the data for plotting 23 | x = np.arange(len(names)) + offset # the label locations with offset 24 | width = 0.125 # the width of the bars 25 | 26 | # Plot Time for 5k requests 27 | rects = ax.bar(x, time_5k, width, label="Time 5k") 28 | ax.bar_label(rects, padding=3, fontsize=7, rotation=90) 29 | 30 | # Plot Time for 50k requests 31 | rects = ax.bar(x + width, time_50k, width, label="Time 50k") 32 | ax.bar_label(rects, padding=3, fontsize=7, rotation=90) 33 | 34 | # Plot Time for 200k requests 35 | rects = ax.bar(x + 2 * width, time_200k, width, label="Time 200k") 36 | ax.bar_label(rects, padding=3, fontsize=7, rotation=90) 37 | 38 | # Plot CPU time for 5k requests 39 | rects = ax.bar(x + 3 * width, cpu_time_5k, width, label="CPU Time 5k") 40 | ax.bar_label(rects, padding=3, fontsize=7, rotation=90) 41 | 42 | # Plot CPU time for 50k requests 43 | rects = ax.bar(x + 4 * width, cpu_time_50k, width, label="CPU Time 50k") 44 | ax.bar_label(rects, padding=3, fontsize=7, rotation=90) 45 | 46 | # Plot CPU time for 200k requests 47 | rects = ax.bar(x + 5 * width, cpu_time_200k, width, label="CPU Time 200k") 48 | ax.bar_label(rects, padding=3, fontsize=7, rotation=90) 49 | 50 | return x, width, names 51 | 52 | 53 | # Create a figure with three subplots 54 | fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 7), layout="constrained") 55 | 56 | x1, width, names = plot_data("session=False.csv", ax1, 0) 57 | x2, _, _ = plot_data("session=True.csv", ax2, 0) 58 | x3, _, x3names = plot_data("session='Async'.csv", ax3, 0) 59 | 60 | 61 | # Adjust the y-axis limits for the first subplot 62 | y_min, y_max = ax1.get_ylim() 63 | new_y_max = y_max + 7 64 | ax1.set_ylim(y_min, new_y_max) 65 | 66 | # Adjust the y-axis limits for the second subplot 67 | y_min, y_max = ax2.get_ylim() 68 | new_y_max = y_max + 2 69 | ax2.set_ylim(y_min, new_y_max) 70 | 71 | # Adjust the y-axis limits for the third subplot 72 | y_min, y_max = ax3.get_ylim() 73 | new_y_max = y_max + 2 74 | ax3.set_ylim(y_min, new_y_max) 75 | 76 | # Add some text for labels, title and custom x-axis tick labels, etc. 77 | ax1.set_ylabel("Time (s)") 78 | ax1.set_title( 79 | "Benchmark get(url).text | Session=False | Requests: 400 | Response: gzip, utf-8, size 5Kb,50Kb,200Kb" 80 | ) 81 | ax1.set_xticks( 82 | x1 + 3 * width - width / 2 83 | ) # Adjust the x-ticks to be after the 3rd bar, moved 0.5 bar width to the left 84 | ax1.set_xticklabels(names) 85 | ax1.legend(loc="upper left", ncols=6, prop={"size": 8}) 86 | ax1.tick_params(axis="x", labelsize=8) 87 | 88 | ax2.set_ylabel("Time (s)") 89 | ax2.set_title( 90 | "Benchmark get(url).text | Session=True | Requests: 400 | Response: gzip, utf-8, size 5Kb,50Kb,200Kb" 91 | ) 92 | ax2.set_xticks( 93 | x2 + 3 * width - width / 2 94 | ) # Adjust the x-ticks to be after the 3rd bar, moved 0.5 bar width to the left 95 | ax2.set_xticklabels(names) 96 | ax2.legend(loc="upper left", ncols=6, prop={"size": 8}) 97 | ax2.tick_params(axis="x", labelsize=8) 98 | 99 | ax3.set_ylabel("Time (s)") 100 | ax3.set_title( 101 | "Benchmark get(url).text | Session=Async | Requests: 400 | Response: gzip, utf-8, size 5Kb,50Kb,200Kb" 102 | ) 103 | ax3.set_xticks( 104 | x3 + 3 * width - width / 2 105 | ) # Adjust the x-ticks to be after the 3rd bar, moved 0.5 bar width to the left 106 | ax3.set_xticklabels(x3names) 107 | ax3.legend(loc="upper left", ncols=6, prop={"size": 8}) 108 | ax3.tick_params(axis="x", labelsize=8) 109 | 110 | # Save the plot to a file 111 | plt.savefig("benchmark.jpg", format="jpg", dpi=80, bbox_inches="tight") 112 | -------------------------------------------------------------------------------- /primp/primp.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import Any, Iterator, Literal, TypedDict 5 | 6 | if sys.version_info <= (3, 11): 7 | from typing_extensions import Unpack 8 | else: 9 | from typing import Unpack 10 | 11 | HttpMethod = Literal["GET", "HEAD", "OPTIONS", "DELETE", "POST", "PUT", "PATCH"] 12 | IMPERSONATE = Literal[ 13 | "chrome_100", "chrome_101", "chrome_104", "chrome_105", "chrome_106", 14 | "chrome_107", "chrome_108", "chrome_109", "chrome_114", "chrome_116", 15 | "chrome_117", "chrome_118", "chrome_119", "chrome_120", "chrome_123", 16 | "chrome_124", "chrome_126", "chrome_127", "chrome_128", "chrome_129", 17 | "chrome_130", "chrome_131", "chrome_133", 18 | "safari_15.3", "safari_15.5", "safari_15.6.1", "safari_16", 19 | "safari_16.5", "safari_17.0", "safari_17.2.1", "safari_17.4.1", 20 | "safari_17.5", "safari_18", "safari_18.2", 21 | "safari_ios_16.5", "safari_ios_17.2", "safari_ios_17.4.1", "safari_ios_18.1.1", 22 | "safari_ipad_18", 23 | "okhttp_3.9", "okhttp_3.11", "okhttp_3.13", "okhttp_3.14", "okhttp_4.9", 24 | "okhttp_4.10", "okhttp_5", 25 | "edge_101", "edge_122", "edge_127", "edge_131", 26 | "firefox_109", "firefox_117", "firefox_128", "firefox_133", "firefox_135", 27 | "random", 28 | ] # fmt: skip 29 | IMPERSONATE_OS = Literal["android", "ios", "linux", "macos", "windows", "random"] 30 | 31 | class RequestParams(TypedDict, total=False): 32 | auth: tuple[str, str | None] | None 33 | auth_bearer: str | None 34 | params: dict[str, str] | None 35 | headers: dict[str, str] | None 36 | cookies: dict[str, str] | None 37 | timeout: float | None 38 | content: bytes | None 39 | data: dict[str, Any] | None 40 | json: Any | None 41 | files: dict[str, str] | None 42 | 43 | class ClientRequestParams(RequestParams): 44 | impersonate: IMPERSONATE | None 45 | impersonate_os: IMPERSONATE_OS | None 46 | verify: bool | None 47 | ca_cert_file: str | None 48 | 49 | class Response: 50 | @property 51 | def content(self) -> bytes: ... 52 | @property 53 | def cookies(self) -> dict[str, str]: ... 54 | @property 55 | def headers(self) -> dict[str, str]: ... 56 | @property 57 | def status_code(self) -> int: ... 58 | @property 59 | def url(self) -> str: ... 60 | @property 61 | def encoding(self) -> str: ... 62 | @property 63 | def text(self) -> str: ... 64 | def json(self) -> Any: ... 65 | def stream(self) -> Iterator[bytes]: ... 66 | @property 67 | def text_markdown(self) -> str: ... 68 | @property 69 | def text_plain(self) -> str: ... 70 | @property 71 | def text_rich(self) -> str: ... 72 | 73 | class RClient: 74 | def __init__( 75 | self, 76 | auth: tuple[str, str | None] | None = None, 77 | auth_bearer: str | None = None, 78 | params: dict[str, str] | None = None, 79 | headers: dict[str, str] | None = None, 80 | timeout: float | None = None, 81 | cookie_store: bool | None = True, 82 | referer: bool | None = True, 83 | proxy: str | None = None, 84 | impersonate: IMPERSONATE | None = None, 85 | impersonate_os: IMPERSONATE_OS | None = None, 86 | follow_redirects: bool | None = True, 87 | max_redirects: int | None = 20, 88 | verify: bool | None = True, 89 | ca_cert_file: str | None = None, 90 | https_only: bool | None = False, 91 | http2_only: bool | None = False, 92 | ): ... 93 | @property 94 | def headers(self) -> dict[str, str]: ... 95 | @headers.setter 96 | def headers(self, headers: dict[str, str]) -> None: ... 97 | def headers_update(self, headers: dict[str, str]) -> None: ... 98 | def get_cookies(self, url: str) -> dict[str, str]: ... 99 | def set_cookies(self, url: str, cookies: dict[str, str]) -> None: ... 100 | @property 101 | def proxy(self) -> str | None: ... 102 | @proxy.setter 103 | def proxy(self, proxy: str) -> None: ... 104 | @property 105 | def impersonate(self) -> str | None: ... 106 | @impersonate.setter 107 | def impersonate(self, impersonate: IMPERSONATE) -> None: ... 108 | @property 109 | def impersonate_os(self) -> str | None: ... 110 | @impersonate_os.setter 111 | def impersonate_os(self, impersonate: IMPERSONATE_OS) -> None: ... 112 | def request(self, method: HttpMethod, url: str, **kwargs: Unpack[RequestParams]) -> Response: ... 113 | def get(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: ... 114 | def head(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: ... 115 | def options(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: ... 116 | def delete(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: ... 117 | def post(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: ... 118 | def put(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: ... 119 | def patch(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: ... 120 | 121 | def request(method: HttpMethod, url: str, **kwargs: Unpack[ClientRequestParams]) -> Response: ... 122 | def get(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response: ... 123 | def head(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response: ... 124 | def options(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response: ... 125 | def delete(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response: ... 126 | def post(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response: ... 127 | def put(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response: ... 128 | def patch(url: str, **kwargs: Unpack[ClientRequestParams]) -> Response: ... 129 | -------------------------------------------------------------------------------- /src/impersonate.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use rand::prelude::*; 3 | use rquest::{Impersonate, ImpersonateOS}; 4 | 5 | pub const IMPERSONATE_LIST: &[Impersonate] = &[ 6 | Impersonate::Chrome100, 7 | Impersonate::Chrome101, 8 | Impersonate::Chrome104, 9 | Impersonate::Chrome105, 10 | Impersonate::Chrome106, 11 | Impersonate::Chrome107, 12 | Impersonate::Chrome108, 13 | Impersonate::Chrome109, 14 | Impersonate::Chrome114, 15 | Impersonate::Chrome116, 16 | Impersonate::Chrome117, 17 | Impersonate::Chrome118, 18 | Impersonate::Chrome119, 19 | Impersonate::Chrome120, 20 | Impersonate::Chrome123, 21 | Impersonate::Chrome124, 22 | Impersonate::Chrome126, 23 | Impersonate::Chrome127, 24 | Impersonate::Chrome128, 25 | Impersonate::Chrome129, 26 | Impersonate::Chrome130, 27 | Impersonate::Chrome131, 28 | Impersonate::Chrome133, 29 | Impersonate::SafariIos16_5, 30 | Impersonate::SafariIos17_2, 31 | Impersonate::SafariIos17_4_1, 32 | Impersonate::SafariIos18_1_1, 33 | Impersonate::SafariIPad18, 34 | Impersonate::Safari15_3, 35 | Impersonate::Safari15_5, 36 | Impersonate::Safari15_6_1, 37 | Impersonate::Safari16, 38 | Impersonate::Safari16_5, 39 | Impersonate::Safari17_0, 40 | Impersonate::Safari17_2_1, 41 | Impersonate::Safari17_4_1, 42 | Impersonate::Safari17_5, 43 | Impersonate::Safari18, 44 | Impersonate::Safari18_2, 45 | //Impersonate::OkHttp3_9, 46 | //Impersonate::OkHttp3_11, 47 | Impersonate::OkHttp3_13, 48 | Impersonate::OkHttp3_14, 49 | Impersonate::OkHttp4_9, 50 | Impersonate::OkHttp4_10, 51 | Impersonate::OkHttp5, 52 | Impersonate::Edge101, 53 | Impersonate::Edge122, 54 | Impersonate::Edge127, 55 | Impersonate::Edge131, 56 | Impersonate::Firefox109, 57 | Impersonate::Firefox117, 58 | Impersonate::Firefox128, 59 | Impersonate::Firefox133, 60 | Impersonate::Firefox135, 61 | ]; 62 | pub const IMPERSONATEOS_LIST: &[ImpersonateOS] = &[ 63 | ImpersonateOS::Android, 64 | ImpersonateOS::IOS, 65 | ImpersonateOS::Linux, 66 | ImpersonateOS::MacOS, 67 | ImpersonateOS::Windows, 68 | ]; 69 | 70 | pub fn get_random_element(input_vec: &[T]) -> &T { 71 | input_vec.choose(&mut rand::rng()).unwrap() 72 | } 73 | 74 | pub trait ImpersonateFromStr { 75 | fn from_str(s: &str) -> Result; 76 | } 77 | 78 | impl ImpersonateFromStr for Impersonate { 79 | fn from_str(s: &str) -> Result { 80 | match s { 81 | "chrome_100" => Ok(Impersonate::Chrome100), 82 | "chrome_101" => Ok(Impersonate::Chrome101), 83 | "chrome_104" => Ok(Impersonate::Chrome104), 84 | "chrome_105" => Ok(Impersonate::Chrome105), 85 | "chrome_106" => Ok(Impersonate::Chrome106), 86 | "chrome_107" => Ok(Impersonate::Chrome107), 87 | "chrome_108" => Ok(Impersonate::Chrome108), 88 | "chrome_109" => Ok(Impersonate::Chrome109), 89 | "chrome_114" => Ok(Impersonate::Chrome114), 90 | "chrome_116" => Ok(Impersonate::Chrome116), 91 | "chrome_117" => Ok(Impersonate::Chrome117), 92 | "chrome_118" => Ok(Impersonate::Chrome118), 93 | "chrome_119" => Ok(Impersonate::Chrome119), 94 | "chrome_120" => Ok(Impersonate::Chrome120), 95 | "chrome_123" => Ok(Impersonate::Chrome123), 96 | "chrome_124" => Ok(Impersonate::Chrome124), 97 | "chrome_126" => Ok(Impersonate::Chrome126), 98 | "chrome_127" => Ok(Impersonate::Chrome127), 99 | "chrome_128" => Ok(Impersonate::Chrome128), 100 | "chrome_129" => Ok(Impersonate::Chrome129), 101 | "chrome_130" => Ok(Impersonate::Chrome130), 102 | "chrome_131" => Ok(Impersonate::Chrome131), 103 | "chrome_133" => Ok(Impersonate::Chrome133), 104 | "safari_ios_16.5" => Ok(Impersonate::SafariIos16_5), 105 | "safari_ios_17.2" => Ok(Impersonate::SafariIos17_2), 106 | "safari_ios_17.4.1" => Ok(Impersonate::SafariIos17_4_1), 107 | "safari_ios_18.1.1" => Ok(Impersonate::SafariIos18_1_1), 108 | "safari_ipad_18" => Ok(Impersonate::SafariIPad18), 109 | "safari_15.3" => Ok(Impersonate::Safari15_3), 110 | "safari_15.5" => Ok(Impersonate::Safari15_5), 111 | "safari_15.6.1" => Ok(Impersonate::Safari15_6_1), 112 | "safari_16" => Ok(Impersonate::Safari16), 113 | "safari_16.5" => Ok(Impersonate::Safari16_5), 114 | "safari_17.0" => Ok(Impersonate::Safari17_0), 115 | "safari_17.2.1" => Ok(Impersonate::Safari17_2_1), 116 | "safari_17.4.1" => Ok(Impersonate::Safari17_4_1), 117 | "safari_17.5" => Ok(Impersonate::Safari17_5), 118 | "safari_18" => Ok(Impersonate::Safari18), 119 | "safari_18.2" => Ok(Impersonate::Safari18_2), 120 | "okhttp_3.9" => Ok(Impersonate::OkHttp3_9), 121 | "okhttp_3.11" => Ok(Impersonate::OkHttp3_11), 122 | "okhttp_3.13" => Ok(Impersonate::OkHttp3_13), 123 | "okhttp_3.14" => Ok(Impersonate::OkHttp3_14), 124 | "okhttp_4.9" => Ok(Impersonate::OkHttp4_9), 125 | "okhttp_4.10" => Ok(Impersonate::OkHttp4_10), 126 | "okhttp_5" => Ok(Impersonate::OkHttp5), 127 | "edge_101" => Ok(Impersonate::Edge101), 128 | "edge_122" => Ok(Impersonate::Edge122), 129 | "edge_127" => Ok(Impersonate::Edge127), 130 | "edge_131" => Ok(Impersonate::Edge131), 131 | "firefox_109" => Ok(Impersonate::Firefox109), 132 | "firefox_117" => Ok(Impersonate::Firefox117), 133 | "firefox_128" => Ok(Impersonate::Firefox128), 134 | "firefox_133" => Ok(Impersonate::Firefox133), 135 | "firefox_135" => Ok(Impersonate::Firefox135), 136 | "random" => Ok(*get_random_element(IMPERSONATE_LIST)), 137 | _ => Err(anyhow!("Invalid impersonate: {:?}", s)), 138 | } 139 | } 140 | } 141 | 142 | pub trait ImpersonateOSFromStr { 143 | fn from_str(s: &str) -> Result; 144 | } 145 | 146 | impl ImpersonateOSFromStr for ImpersonateOS { 147 | fn from_str(s: &str) -> Result { 148 | match s { 149 | "android" => Ok(ImpersonateOS::Android), 150 | "ios" => Ok(ImpersonateOS::IOS), 151 | "linux" => Ok(ImpersonateOS::Linux), 152 | "macos" => Ok(ImpersonateOS::MacOS), 153 | "windows" => Ok(ImpersonateOS::Windows), 154 | "random" => Ok(*get_random_element(IMPERSONATEOS_LIST)), 155 | _ => Err(anyhow!("Invalid impersonate_os: {:?}", s)), 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /benchmark/benchmark.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from concurrent.futures import ThreadPoolExecutor, as_completed 4 | from importlib.metadata import version 5 | from io import BytesIO 6 | 7 | import aiohttp 8 | import curl_cffi.requests 9 | import httpx 10 | import pandas as pd 11 | import pycurl 12 | import requests 13 | import tls_client 14 | 15 | import primp 16 | 17 | 18 | class PycurlSession: 19 | def __init__(self): 20 | self.c = pycurl.Curl() 21 | self.content = None 22 | 23 | def __del__(self): 24 | self.close() 25 | 26 | def close(self): 27 | self.c.close() 28 | 29 | def get(self, url): 30 | buffer = BytesIO() 31 | self.c.setopt(pycurl.URL, url) 32 | self.c.setopt(pycurl.WRITEDATA, buffer) 33 | self.c.setopt(pycurl.ENCODING, "gzip") # Automatically handle gzip encoding 34 | self.c.perform() 35 | self.content = buffer.getvalue() 36 | return self 37 | 38 | @property 39 | def text(self): 40 | return self.content.decode("utf-8") 41 | 42 | 43 | results = [] 44 | PACKAGES = [ 45 | ("requests", requests.Session), 46 | ("httpx", httpx.Client), 47 | ("tls_client", tls_client.Session), 48 | ("curl_cffi", curl_cffi.requests.Session), 49 | ("pycurl", PycurlSession), 50 | ("primp", primp.Client), 51 | ] 52 | AsyncPACKAGES = [ 53 | ("aiohttp", aiohttp.ClientSession), 54 | ("httpx", httpx.AsyncClient), 55 | ("curl_cffi", curl_cffi.requests.AsyncSession), 56 | ("primp", primp.AsyncClient), 57 | ] 58 | 59 | 60 | def add_package_version(packages): 61 | return [(f"{name} {version(name)}", classname) for name, classname in packages] 62 | 63 | 64 | def get_test(session_class, requests_number): 65 | for _ in range(requests_number): 66 | s = session_class() 67 | try: 68 | s.get(url).text 69 | finally: 70 | if hasattr(s, "close"): 71 | s.close() 72 | 73 | 74 | def session_get_test(session_class, requests_number): 75 | s = session_class() 76 | try: 77 | for _ in range(requests_number): 78 | s.get(url).text 79 | finally: 80 | if hasattr(s, "close"): 81 | s.close() 82 | 83 | 84 | async def async_session_get_test(session_class, requests_number): 85 | async def aget(s, url): 86 | if session_class.__module__ == "aiohttp.client": 87 | async with s.get(url) as resp: 88 | text = await resp.text() 89 | return text 90 | else: 91 | resp = await s.get(url) 92 | return resp.text 93 | 94 | async with session_class() as s: 95 | tasks = [aget(s, url) for _ in range(requests_number)] 96 | await asyncio.gather(*tasks) 97 | 98 | 99 | PACKAGES = add_package_version(PACKAGES) 100 | AsyncPACKAGES = add_package_version(AsyncPACKAGES) 101 | requests_number = 400 102 | 103 | # Sync 104 | for session in [False, True]: 105 | for response_size in ["5k", "50k", "200k"]: 106 | url = f"http://127.0.0.1:8000/{response_size}" 107 | print(f"\nThreads=1, {session=}, {response_size=}, {requests_number=}") 108 | for name, session_class in PACKAGES: 109 | start = time.perf_counter() 110 | cpu_start = time.process_time() 111 | if session: 112 | session_get_test(session_class, requests_number) 113 | else: 114 | get_test(session_class, requests_number) 115 | dur = round(time.perf_counter() - start, 2) 116 | cpu_dur = round(time.process_time() - cpu_start, 2) 117 | results.append( 118 | { 119 | "name": name, 120 | "session": session, 121 | "size": response_size, 122 | "time": dur, 123 | "cpu_time": cpu_dur, 124 | } 125 | ) 126 | print(f" name: {name:<30} time: {dur} cpu_time: {cpu_dur}") 127 | 128 | # Async 129 | for response_size in ["5k", "50k", "200k"]: 130 | url = f"http://127.0.0.1:8000/{response_size}" 131 | print(f"\nThreads=1, session=Async, {response_size=}, {requests_number=}") 132 | 133 | for name, session_class in AsyncPACKAGES: 134 | start = time.perf_counter() 135 | cpu_start = time.process_time() 136 | asyncio.run(async_session_get_test(session_class, requests_number)) 137 | dur = round(time.perf_counter() - start, 2) 138 | cpu_dur = round(time.process_time() - cpu_start, 2) 139 | 140 | results.append( 141 | { 142 | "name": name, 143 | "session": "Async", 144 | "size": response_size, 145 | "time": dur, 146 | "cpu_time": cpu_dur, 147 | } 148 | ) 149 | 150 | print(f" name: {name:<30} time: {dur} cpu_time: {cpu_dur}") 151 | 152 | df = pd.DataFrame(results) 153 | pivot_df = df.pivot_table( 154 | index=["name", "session"], 155 | columns="size", 156 | values=["time", "cpu_time"], 157 | aggfunc="mean", 158 | ) 159 | pivot_df.reset_index(inplace=True) 160 | pivot_df.columns = [" ".join(col).strip() for col in pivot_df.columns.values] 161 | pivot_df = pivot_df[["name", "session"] + [col for col in pivot_df.columns if col not in ["name", "session"]]] 162 | print(pivot_df) 163 | 164 | for session in [False, True, "Async"]: 165 | session_df = pivot_df[pivot_df["session"] == session] 166 | print(f"\nThreads=1 {session=}:") 167 | print(session_df.to_string(index=False)) 168 | session_df.to_csv(f"{session=}.csv", index=False) 169 | 170 | ######################################################## 171 | # Not for generating image, just to check multithreading working 172 | # Multiple threads 173 | threads_numbers = [5, 32] 174 | for threads_number in threads_numbers: 175 | for response_size in ["5k", "50k", "200k"]: 176 | url = f"http://127.0.0.1:8000/{response_size}" 177 | print(f"\nThreads={threads_number}, session=True, {response_size=}, {requests_number=}") 178 | for name, session_class in PACKAGES: 179 | start = time.perf_counter() 180 | cpu_start = time.process_time() 181 | with ThreadPoolExecutor(threads_number) as executor: 182 | futures = [ 183 | executor.submit( 184 | session_get_test, 185 | session_class, 186 | int(requests_number / threads_number), 187 | ) 188 | for _ in range(threads_number) 189 | ] 190 | for f in as_completed(futures): 191 | f.result() 192 | dur = round(time.perf_counter() - start, 2) 193 | cpu_dur = round(time.process_time() - cpu_start, 2) 194 | results.append( 195 | { 196 | "name": name, 197 | "threads": threads_number, 198 | "size": response_size, 199 | "time": dur, 200 | "cpu_time": cpu_dur, 201 | } 202 | ) 203 | print(f" name: {name:<30} time: {dur} cpu_time: {cpu_dur}") 204 | 205 | 206 | df = pd.DataFrame(results) 207 | pivot_df = df.pivot_table( 208 | index=["name", "threads"], 209 | columns="size", 210 | values=["time", "cpu_time"], 211 | aggfunc="mean", 212 | ) 213 | pivot_df.reset_index(inplace=True) 214 | pivot_df.columns = [" ".join(col).strip() for col in pivot_df.columns.values] 215 | pivot_df = pivot_df[["name", "threads"] + [col for col in pivot_df.columns if col not in ["name", "threads"]]] 216 | unique_threads = pivot_df["threads"].unique() 217 | for thread in unique_threads: 218 | thread_df = pivot_df[pivot_df["threads"] == thread] 219 | print(f"\nThreads={thread} session=True") 220 | print(thread_df.to_string(index=False)) 221 | -------------------------------------------------------------------------------- /src/response.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::Result; 4 | use encoding_rs::{Encoding, UTF_8}; 5 | use html2text::{ 6 | from_read, from_read_with_decorator, 7 | render::{RichDecorator, TrivialDecorator}, 8 | }; 9 | use http_body_util::BodyExt; 10 | use mime::Mime; 11 | use pyo3::{ 12 | prelude::*, 13 | types::{PyBytes, PyDict, PyString}, 14 | IntoPyObjectExt, 15 | }; 16 | use pythonize::pythonize; 17 | use serde_json::from_slice; 18 | use tokio::sync::Mutex as TMutex; 19 | 20 | use crate::RUNTIME; 21 | 22 | /// A struct representing an HTTP response. 23 | /// 24 | /// This struct provides methods to access various parts of an HTTP response, such as headers, cookies, status code, and the response body. 25 | /// It also supports decoding the response body as text or JSON, with the ability to specify the character encoding. 26 | #[pyclass] 27 | pub struct Response { 28 | pub resp: http::Response, 29 | pub _content: Option>, 30 | pub _encoding: Option, 31 | pub _headers: Option>, 32 | pub _cookies: Option>, 33 | #[pyo3(get)] 34 | pub url: String, 35 | #[pyo3(get)] 36 | pub status_code: u16, 37 | } 38 | 39 | #[pymethods] 40 | impl Response { 41 | #[getter] 42 | fn get_content<'rs>(&mut self, py: Python<'rs>) -> Result> { 43 | if let Some(content) = &self._content { 44 | let cloned = content.clone_ref(py); 45 | return Ok(cloned.into_bound(py)); 46 | } 47 | 48 | let bytes = py.allow_threads(|| { 49 | RUNTIME.block_on(async { 50 | BodyExt::collect(self.resp.body_mut()) 51 | .await 52 | .map(|buf| buf.to_bytes()) 53 | }) 54 | })?; 55 | 56 | let content = PyBytes::new(py, &bytes); 57 | self._content = Some(content.clone().unbind()); 58 | Ok(content) 59 | } 60 | 61 | #[getter] 62 | fn get_encoding<'rs>(&mut self, py: Python<'rs>) -> Result { 63 | if let Some(encoding) = self._encoding.as_ref() { 64 | return Ok(encoding.clone()); 65 | } 66 | let encoding = py.allow_threads(|| { 67 | self.resp 68 | .headers() 69 | .get(http::header::CONTENT_TYPE) 70 | .and_then(|value| value.to_str().ok()) 71 | .and_then(|value| value.parse::().ok()) 72 | .and_then(|mime| mime.get_param("charset").map(|charset| charset.to_string())) 73 | .unwrap_or("utf-8".to_string()) 74 | }); 75 | self._encoding = Some(encoding.clone()); 76 | Ok(encoding) 77 | } 78 | 79 | #[setter] 80 | fn set_encoding<'rs>(&mut self, encoding: Option) -> Result<()> { 81 | if let Some(encoding) = encoding { 82 | self._encoding = Some(encoding); 83 | } 84 | Ok(()) 85 | } 86 | 87 | #[getter] 88 | fn text<'rs>(&mut self, py: Python<'rs>) -> Result> { 89 | let content = self.get_content(py)?.unbind(); 90 | let encoding = self.get_encoding(py)?; 91 | let raw_bytes = content.as_bytes(py); 92 | let text = py.allow_threads(|| { 93 | let encoding = Encoding::for_label(encoding.as_bytes()).unwrap_or(UTF_8); 94 | let (text, _, _) = encoding.decode(raw_bytes); 95 | text 96 | }); 97 | Ok(text.into_pyobject_or_pyerr(py)?) 98 | } 99 | 100 | fn json<'rs>(&mut self, py: Python<'rs>) -> Result> { 101 | let content = self.get_content(py)?.unbind(); 102 | let raw_bytes = content.as_bytes(py); 103 | let json_value: serde_json::Value = from_slice(raw_bytes)?; 104 | let result = pythonize(py, &json_value)?; 105 | Ok(result) 106 | } 107 | 108 | #[getter] 109 | fn get_headers<'rs>(&mut self, py: Python<'rs>) -> Result> { 110 | if let Some(headers) = &self._headers { 111 | return Ok(headers.clone_ref(py).into_bound(py)); 112 | } 113 | 114 | let new_cookies = PyDict::new(py); 115 | for (key, value) in self.resp.headers() { 116 | new_cookies.set_item(key.as_str(), value.to_str()?)?; 117 | } 118 | self._headers = Some(new_cookies.clone().unbind()); 119 | Ok(new_cookies) 120 | } 121 | 122 | #[getter] 123 | fn get_cookies<'rs>(&mut self, py: Python<'rs>) -> Result> { 124 | if let Some(cookies) = &self._cookies { 125 | return Ok(cookies.clone_ref(py).into_bound(py)); 126 | } 127 | 128 | let new_cookies = PyDict::new(py); 129 | let set_cookie_header = self.resp.headers().get_all(http::header::SET_COOKIE); 130 | for cookie_header in set_cookie_header.iter() { 131 | if let Ok(cookie_str) = cookie_header.to_str() { 132 | if let Some((name, value)) = cookie_str.split_once('=') { 133 | new_cookies 134 | .set_item(name.trim(), value.split(';').next().unwrap_or("").trim())?; 135 | } 136 | } 137 | } 138 | self._cookies = Some(new_cookies.clone().unbind()); 139 | Ok(new_cookies) 140 | } 141 | 142 | #[getter] 143 | fn text_markdown(&mut self, py: Python) -> Result { 144 | let content = self.get_content(py)?.unbind(); 145 | let raw_bytes = content.as_bytes(py); 146 | let text = py.allow_threads(|| from_read(raw_bytes, 100))?; 147 | Ok(text) 148 | } 149 | 150 | #[getter] 151 | fn text_plain(&mut self, py: Python) -> Result { 152 | let content = self.get_content(py)?.unbind(); 153 | let raw_bytes = content.as_bytes(py); 154 | let text = 155 | py.allow_threads(|| from_read_with_decorator(raw_bytes, 100, TrivialDecorator::new()))?; 156 | Ok(text) 157 | } 158 | 159 | #[getter] 160 | fn text_rich(&mut self, py: Python) -> Result { 161 | let content = self.get_content(py)?.unbind(); 162 | let raw_bytes = content.as_bytes(py); 163 | let text = 164 | py.allow_threads(|| from_read_with_decorator(raw_bytes, 100, RichDecorator::new()))?; 165 | Ok(text) 166 | } 167 | } 168 | 169 | #[pyclass] 170 | struct ResponseStream { 171 | resp: Arc>>, 172 | } 173 | 174 | #[pymethods] 175 | impl ResponseStream { 176 | fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { 177 | slf 178 | } 179 | 180 | fn __next__<'rs>(&mut self, py: Python<'rs>) -> PyResult>> { 181 | let chunk = py.allow_threads(|| { 182 | RUNTIME.block_on(async { self.resp.lock().await.body_mut().frame().await.transpose() }) 183 | }); 184 | match chunk { 185 | Ok(Some(frame)) => match frame.into_data() { 186 | Ok(data) => Ok(Some(PyBytes::new(py, data.as_ref()))), 187 | Err(e) => { 188 | let error_msg = format!("Failed to convert frame to data: {:?}", e); 189 | Err(pyo3::exceptions::PyRuntimeError::new_err(error_msg)) 190 | } 191 | }, 192 | Ok(None) => Err(pyo3::exceptions::PyStopIteration::new_err( 193 | "The iterator is exhausted", 194 | )), 195 | Err(e) => { 196 | let error_msg = format!("Failed to read chunk: {:?}", e); 197 | Err(pyo3::exceptions::PyRuntimeError::new_err(error_msg)) 198 | } 199 | } 200 | } 201 | } 202 | 203 | #[pymethods] 204 | impl Response { 205 | fn stream(&mut self, py: Python<'_>) -> PyResult { 206 | self.get_cookies(py)?; 207 | self.get_headers(py)?; 208 | self.get_encoding(py)?; 209 | let mut temp = http::Response::default(); 210 | std::mem::swap(&mut self.resp, &mut temp); 211 | let resp = Arc::new(TMutex::new(temp)); 212 | Ok(ResponseStream { resp }) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import pytest 4 | 5 | import certifi 6 | import primp # type: ignore 7 | 8 | 9 | def retry(max_retries=3, delay=1): 10 | def decorator(func): 11 | def wrapper(*args, **kwargs): 12 | for attempt in range(max_retries): 13 | try: 14 | return func(*args, **kwargs) 15 | except Exception as e: 16 | if attempt < max_retries - 1: 17 | sleep(delay) 18 | continue 19 | else: 20 | raise e 21 | 22 | return wrapper 23 | 24 | return decorator 25 | 26 | 27 | @retry() 28 | def test_client_init_params(): 29 | auth = ("user", "password") 30 | headers = {"X-Test": "test"} 31 | cookies = {"ccc": "ddd", "cccc": "dddd"} 32 | params = {"x": "aaa", "y": "bbb"} 33 | client = primp.Client( 34 | auth=auth, 35 | params=params, 36 | headers=headers, 37 | ca_cert_file=certifi.where(), 38 | ) 39 | client.set_cookies("https://httpbin.org", cookies) 40 | response = client.get("https://httpbin.org/anything") 41 | assert response.status_code == 200 42 | assert response.headers["content-type"] == "application/json" 43 | json_data = response.json() 44 | assert json_data["headers"]["X-Test"] == "test" 45 | assert json_data["headers"]["Authorization"] == "Basic dXNlcjpwYXNzd29yZA==" 46 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 47 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 48 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 49 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 50 | 51 | 52 | @retry() 53 | def test_client_setters(): 54 | client = primp.Client() 55 | client.auth = ("user", "password") 56 | client.headers = {"X-Test": "TesT"} 57 | client.params = {"x": "aaa", "y": "bbb"} 58 | client.timeout = 20 59 | 60 | client.set_cookies("https://httpbin.org", {"ccc": "ddd", "cccc": "dddd"}) 61 | assert client.get_cookies("https://httpbin.org/anything") == {"ccc": "ddd", "cccc": "dddd"} 62 | 63 | response = client.get("https://httpbin.org/anything") 64 | assert response.status_code == 200 65 | assert response.status_code == 200 66 | assert client.auth == ("user", "password") 67 | assert client.headers == {"x-test": "TesT"} 68 | assert client.params == {"x": "aaa", "y": "bbb"} 69 | assert client.timeout == 20.0 70 | json_data = response.json() 71 | assert json_data["method"] == "GET" 72 | assert json_data["headers"]["X-Test"] == "TesT" 73 | assert json_data["headers"]["Authorization"] == "Basic dXNlcjpwYXNzd29yZA==" 74 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 75 | assert "Basic dXNlcjpwYXNzd29yZA==" in response.text 76 | assert b"Basic dXNlcjpwYXNzd29yZA==" in response.content 77 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 78 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 79 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 80 | 81 | 82 | @retry() 83 | def test_client_request_get(): 84 | client = primp.Client() 85 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 86 | headers = {"X-Test": "test"} 87 | cookies = {"ccc": "ddd", "cccc": "dddd"} 88 | params = {"x": "aaa", "y": "bbb"} 89 | client.set_cookies("https://httpbin.org", cookies) 90 | response = client.request( 91 | "GET", 92 | "https://httpbin.org/anything", 93 | auth_bearer=auth_bearer, 94 | headers=headers, 95 | params=params, 96 | ) 97 | assert response.status_code == 200 98 | json_data = response.json() 99 | assert json_data["method"] == "GET" 100 | assert json_data["headers"]["X-Test"] == "test" 101 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 102 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 103 | assert "Bearer bearerXXXXXXXXXXXXXXXXXXXX" in response.text 104 | assert b"Bearer bearerXXXXXXXXXXXXXXXXXXXX" in response.content 105 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 106 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 107 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 108 | 109 | 110 | @retry() 111 | def test_client_get(): 112 | client = primp.Client() 113 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 114 | headers = {"X-Test": "test"} 115 | cookies = {"ccc": "ddd", "cccc": "dddd"} 116 | params = {"x": "aaa", "y": "bbb"} 117 | client.set_cookies("https://httpbin.org", cookies) 118 | response = client.get( 119 | "https://httpbin.org/anything", 120 | auth_bearer=auth_bearer, 121 | headers=headers, 122 | params=params, 123 | ) 124 | assert response.status_code == 200 125 | json_data = response.json() 126 | assert json_data["method"] == "GET" 127 | assert json_data["headers"]["X-Test"] == "test" 128 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 129 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 130 | assert "Bearer bearerXXXXXXXXXXXXXXXXXXXX" in response.text 131 | assert b"Bearer bearerXXXXXXXXXXXXXXXXXXXX" in response.content 132 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 133 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 134 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 135 | 136 | 137 | @retry() 138 | def test_client_post_content(): 139 | client = primp.Client() 140 | auth = ("user", "password") 141 | headers = {"X-Test": "test"} 142 | cookies = {"ccc": "ddd", "cccc": "dddd"} 143 | params = {"x": "aaa", "y": "bbb"} 144 | content = b"test content" 145 | client.set_cookies("https://httpbin.org", cookies) 146 | response = client.post( 147 | "https://httpbin.org/anything", 148 | auth=auth, 149 | headers=headers, 150 | params=params, 151 | content=content, 152 | ) 153 | assert response.status_code == 200 154 | json_data = response.json() 155 | assert json_data["method"] == "POST" 156 | assert json_data["headers"]["X-Test"] == "test" 157 | assert json_data["headers"]["Authorization"] == "Basic dXNlcjpwYXNzd29yZA==" 158 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 159 | assert json_data["data"] == "test content" 160 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 161 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 162 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 163 | 164 | 165 | @retry() 166 | def test_client_post_data(): 167 | client = primp.Client() 168 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 169 | headers = {"X-Test": "test"} 170 | cookies = {"ccc": "ddd", "cccc": "dddd"} 171 | params = {"x": "aaa", "y": "bbb"} 172 | data = {"key1": "value1", "key2": "value2"} 173 | client.set_cookies("https://httpbin.org", cookies) 174 | response = client.post( 175 | "https://httpbin.org/anything", 176 | auth_bearer=auth_bearer, 177 | headers=headers, 178 | params=params, 179 | data=data, 180 | ) 181 | assert response.status_code == 200 182 | json_data = response.json() 183 | assert json_data["method"] == "POST" 184 | assert json_data["headers"]["X-Test"] == "test" 185 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 186 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 187 | assert json_data["form"] == {"key1": "value1", "key2": "value2"} 188 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 189 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 190 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 191 | 192 | 193 | @retry() 194 | def test_client_post_json(): 195 | client = primp.Client() 196 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 197 | headers = {"X-Test": "test"} 198 | cookies = {"ccc": "ddd", "cccc": "dddd"} 199 | params = {"x": "aaa", "y": "bbb"} 200 | data = {"key1": "value1", "key2": "value2"} 201 | client.set_cookies("https://httpbin.org", cookies) 202 | response = client.post( 203 | "https://httpbin.org/anything", 204 | auth_bearer=auth_bearer, 205 | headers=headers, 206 | params=params, 207 | json=data, 208 | ) 209 | assert response.status_code == 200 210 | json_data = response.json() 211 | assert json_data["method"] == "POST" 212 | assert json_data["headers"]["X-Test"] == "test" 213 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 214 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 215 | assert json_data["json"] == data 216 | # temp 217 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 218 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 219 | 220 | 221 | @pytest.fixture(scope="session") 222 | def test_files(tmp_path_factory): 223 | tmp_path_factory.mktemp("data") 224 | temp_file1 = tmp_path_factory.mktemp("data") / "img1.png" 225 | with open(temp_file1, "w") as f: 226 | f.write("aaa111") 227 | temp_file2 = tmp_path_factory.mktemp("data") / "img2.png" 228 | with open(temp_file2, "w") as f: 229 | f.write("bbb222") 230 | return str(temp_file1), str(temp_file2) 231 | 232 | 233 | def test_client_post_files(test_files): 234 | temp_file1, temp_file2 = test_files 235 | client = primp.Client() 236 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 237 | headers = {"X-Test": "test"} 238 | cookies = {"ccc": "ddd", "cccc": "dddd"} 239 | params = {"x": "aaa", "y": "bbb"} 240 | files = {"file1": temp_file1, "file2": temp_file2} 241 | client.set_cookies("https://httpbin.org", cookies) 242 | response = client.post( 243 | "https://httpbin.org/anything", 244 | auth_bearer=auth_bearer, 245 | headers=headers, 246 | params=params, 247 | files=files, 248 | ) 249 | assert response.status_code == 200 250 | json_data = response.json() 251 | assert json_data["method"] == "POST" 252 | assert json_data["headers"]["X-Test"] == "test" 253 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 254 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 255 | assert json_data["files"] == {"file1": "aaa111", "file2": "bbb222"} 256 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 257 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 258 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 259 | 260 | 261 | @retry() 262 | def test_client_impersonate_chrome131(): 263 | client = primp.Client( 264 | impersonate="chrome_131", 265 | impersonate_os="windows", 266 | ) 267 | response = client.get("https://tls.peet.ws/api/all") 268 | #response = client.get("https://tls.http.rw/api/all") 269 | assert response.status_code == 200 270 | json_data = response.json() 271 | assert ( 272 | json_data["user_agent"] 273 | == "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" 274 | ) 275 | assert json_data["tls"]["ja4"] == "t13d1516h2_8daaf6152771_b1ff8ab2d16f" 276 | assert ( 277 | json_data["http2"]["akamai_fingerprint_hash"] 278 | == "52d84b11737d980aef856699f885ca86" 279 | ) 280 | assert json_data["tls"]["peetprint_hash"] == "7466733991096b3f4e6c0e79b0083559" 281 | -------------------------------------------------------------------------------- /primp/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import sys 5 | from functools import partial 6 | from typing import TYPE_CHECKING, TypedDict 7 | 8 | if sys.version_info <= (3, 11): 9 | from typing_extensions import Unpack 10 | else: 11 | from typing import Unpack 12 | 13 | 14 | from .primp import RClient 15 | 16 | if TYPE_CHECKING: 17 | from .primp import IMPERSONATE, IMPERSONATE_OS, ClientRequestParams, HttpMethod, RequestParams, Response 18 | else: 19 | 20 | class _Unpack: 21 | @staticmethod 22 | def __getitem__(*args, **kwargs): 23 | pass 24 | 25 | Unpack = _Unpack() 26 | RequestParams = ClientRequestParams = TypedDict 27 | 28 | 29 | class Client(RClient): 30 | """Initializes an HTTP client that can impersonate web browsers.""" 31 | 32 | def __init__( 33 | self, 34 | auth: tuple[str, str | None] | None = None, 35 | auth_bearer: str | None = None, 36 | params: dict[str, str] | None = None, 37 | headers: dict[str, str] | None = None, 38 | cookie_store: bool | None = True, 39 | referer: bool | None = True, 40 | proxy: str | None = None, 41 | timeout: float | None = 30, 42 | impersonate: IMPERSONATE | None = None, 43 | impersonate_os: IMPERSONATE_OS | None = None, 44 | follow_redirects: bool | None = True, 45 | max_redirects: int | None = 20, 46 | verify: bool | None = True, 47 | ca_cert_file: str | None = None, 48 | https_only: bool | None = False, 49 | http2_only: bool | None = False, 50 | ): 51 | """ 52 | Args: 53 | auth: a tuple containing the username and an optional password for basic authentication. Default is None. 54 | auth_bearer: a string representing the bearer token for bearer token authentication. Default is None. 55 | params: a map of query parameters to append to the URL. Default is None. 56 | headers: an optional map of HTTP headers to send with requests. Ignored if `impersonate` is set. 57 | cookie_store: enable a persistent cookie store. Received cookies will be preserved and included 58 | in additional requests. Default is True. 59 | referer: automatic setting of the `Referer` header. Default is True. 60 | proxy: proxy URL for HTTP requests, example: "socks5://127.0.0.1:9150". Default is None. 61 | timeout: timeout for HTTP requests in seconds. Default is 30. 62 | impersonate: impersonate browser. Supported browsers: 63 | "chrome_100", "chrome_101", "chrome_104", "chrome_105", "chrome_106", 64 | "chrome_107", "chrome_108", "chrome_109", "chrome_114", "chrome_116", 65 | "chrome_117", "chrome_118", "chrome_119", "chrome_120", "chrome_123", 66 | "chrome_124", "chrome_126", "chrome_127", "chrome_128", "chrome_129", 67 | "chrome_130", "chrome_131", "chrome_133" 68 | "safari_15.3", "safari_15.5", "safari_15.6.1", "safari_16", 69 | "safari_16.5", "safari_17.0", "safari_17.2.1", "safari_17.4.1", 70 | "safari_17.5", "safari_18", "safari_18.2", 71 | "safari_ios_16.5", "safari_ios_17.2", "safari_ios_17.4.1", "safari_ios_18.1.1", 72 | "safari_ipad_18", 73 | "okhttp_3.9", "okhttp_3.11", "okhttp_3.13", "okhttp_3.14", "okhttp_4.9", 74 | "okhttp_4.10", "okhttp_5", 75 | "edge_101", "edge_122", "edge_127", "edge_131", 76 | "firefox_109", "firefox_117", "firefox_128", "firefox_133", "firefox_135". 77 | Default is None. 78 | impersonate_os: impersonate OS. Supported OS: 79 | "android", "ios", "linux", "macos", "windows". Default is None. 80 | follow_redirects: a boolean to enable or disable following redirects. Default is True. 81 | max_redirects: the maximum number of redirects if `follow_redirects` is True. Default is 20. 82 | verify: an optional boolean indicating whether to verify SSL certificates. Default is True. 83 | ca_cert_file: path to CA certificate store. Default is None. 84 | https_only: restrict the Client to be used with HTTPS only requests. Default is False. 85 | http2_only: if true - use only HTTP/2, if false - use only HTTP/1. Default is False. 86 | """ 87 | super().__init__() 88 | 89 | def __enter__(self) -> Client: 90 | return self 91 | 92 | def __exit__(self, *args): 93 | del self 94 | 95 | def request(self, method: HttpMethod, url: str, **kwargs: Unpack[RequestParams]) -> Response: 96 | return super().request(method=method, url=url, **kwargs) 97 | 98 | def get(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: 99 | return self.request(method="GET", url=url, **kwargs) 100 | 101 | def head(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: 102 | return self.request(method="HEAD", url=url, **kwargs) 103 | 104 | def options(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: 105 | return self.request(method="OPTIONS", url=url, **kwargs) 106 | 107 | def delete(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: 108 | return self.request(method="DELETE", url=url, **kwargs) 109 | 110 | def post(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: 111 | return self.request(method="POST", url=url, **kwargs) 112 | 113 | def put(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: 114 | return self.request(method="PUT", url=url, **kwargs) 115 | 116 | def patch(self, url: str, **kwargs: Unpack[RequestParams]) -> Response: 117 | return self.request(method="PATCH", url=url, **kwargs) 118 | 119 | 120 | class AsyncClient(Client): 121 | def __init__(self, *args, **kwargs): 122 | super().__init__(*args, **kwargs) 123 | 124 | async def __aenter__(self) -> AsyncClient: 125 | return self 126 | 127 | async def __aexit__(self, *args): 128 | del self 129 | 130 | async def _run_sync_asyncio(self, fn, *args, **kwargs): 131 | loop = asyncio.get_running_loop() 132 | return await loop.run_in_executor(None, partial(fn, *args, **kwargs)) 133 | 134 | async def request(self, method: HttpMethod, url: str, **kwargs: Unpack[RequestParams]): # type: ignore 135 | return await self._run_sync_asyncio(super().request, method=method, url=url, **kwargs) 136 | 137 | async def get(self, url: str, **kwargs: Unpack[RequestParams]): # type: ignore 138 | return await self.request(method="GET", url=url, **kwargs) 139 | 140 | async def head(self, url: str, **kwargs: Unpack[RequestParams]): # type: ignore 141 | return await self.request(method="HEAD", url=url, **kwargs) 142 | 143 | async def options(self, url: str, **kwargs: Unpack[RequestParams]): # type: ignore 144 | return await self.request(method="OPTIONS", url=url, **kwargs) 145 | 146 | async def delete(self, url: str, **kwargs: Unpack[RequestParams]): # type: ignore 147 | return await self.request(method="DELETE", url=url, **kwargs) 148 | 149 | async def post(self, url: str, **kwargs: Unpack[RequestParams]): # type: ignore 150 | return await self.request(method="POST", url=url, **kwargs) 151 | 152 | async def put(self, url: str, **kwargs: Unpack[RequestParams]): # type: ignore 153 | return await self.request(method="PUT", url=url, **kwargs) 154 | 155 | async def patch(self, url: str, **kwargs: Unpack[RequestParams]): # type: ignore 156 | return await self.request(method="PATCH", url=url, **kwargs) 157 | 158 | 159 | def request( 160 | method: HttpMethod, 161 | url: str, 162 | impersonate: IMPERSONATE | None = None, 163 | impersonate_os: IMPERSONATE_OS | None = None, 164 | verify: bool | None = True, 165 | ca_cert_file: str | None = None, 166 | **kwargs: Unpack[RequestParams], 167 | ): 168 | """ 169 | Args: 170 | method: the HTTP method to use (e.g., "GET", "POST"). 171 | url: the URL to which the request will be made. 172 | impersonate: impersonate browser. Supported browsers: 173 | "chrome_100", "chrome_101", "chrome_104", "chrome_105", "chrome_106", 174 | "chrome_107", "chrome_108", "chrome_109", "chrome_114", "chrome_116", 175 | "chrome_117", "chrome_118", "chrome_119", "chrome_120", "chrome_123", 176 | "chrome_124", "chrome_126", "chrome_127", "chrome_128", "chrome_129", 177 | "chrome_130", "chrome_131", "chrome_133", 178 | "safari_15.3", "safari_15.5", "safari_15.6.1", "safari_16", 179 | "safari_16.5", "safari_17.0", "safari_17.2.1", "safari_17.4.1", 180 | "safari_17.5", "safari_18", "safari_18.2", 181 | "safari_ios_16.5", "safari_ios_17.2", "safari_ios_17.4.1", "safari_ios_18.1.1", 182 | "safari_ipad_18", 183 | "okhttp_3.9", "okhttp_3.11", "okhttp_3.13", "okhttp_3.14", "okhttp_4.9", 184 | "okhttp_4.10", "okhttp_5", 185 | "edge_101", "edge_122", "edge_127", "edge_131", 186 | "firefox_109", "firefox_117", "firefox_128", "firefox_133", "firefox_135". 187 | Default is None. 188 | impersonate_os: impersonate OS. Supported OS: 189 | "android", "ios", "linux", "macos", "windows". Default is None. 190 | verify: an optional boolean indicating whether to verify SSL certificates. Default is True. 191 | ca_cert_file: path to CA certificate store. Default is None. 192 | auth: a tuple containing the username and an optional password for basic authentication. Default is None. 193 | auth_bearer: a string representing the bearer token for bearer token authentication. Default is None. 194 | params: a map of query parameters to append to the URL. Default is None. 195 | headers: an optional map of HTTP headers to send with requests. If `impersonate` is set, this will be ignored. 196 | cookies: an optional map of cookies to send with requests as the `Cookie` header. 197 | timeout: the timeout for the request in seconds. Default is 30. 198 | content: the content to send in the request body as bytes. Default is None. 199 | data: the form data to send in the request body. Default is None. 200 | json: a JSON serializable object to send in the request body. Default is None. 201 | files: a map of file fields to file paths to be sent as multipart/form-data. Default is None. 202 | """ 203 | with Client( 204 | impersonate=impersonate, 205 | impersonate_os=impersonate_os, 206 | verify=verify, 207 | ca_cert_file=ca_cert_file, 208 | ) as client: 209 | return client.request(method, url, **kwargs) 210 | 211 | 212 | def get(url: str, **kwargs: Unpack[ClientRequestParams]): 213 | return request(method="GET", url=url, **kwargs) 214 | 215 | 216 | def head(url: str, **kwargs: Unpack[ClientRequestParams]): 217 | return request(method="HEAD", url=url, **kwargs) 218 | 219 | 220 | def options(url: str, **kwargs: Unpack[ClientRequestParams]): 221 | return request(method="OPTIONS", url=url, **kwargs) 222 | 223 | 224 | def delete(url: str, **kwargs: Unpack[ClientRequestParams]): 225 | return request(method="DELETE", url=url, **kwargs) 226 | 227 | 228 | def post(url: str, **kwargs: Unpack[ClientRequestParams]): 229 | return request(method="POST", url=url, **kwargs) 230 | 231 | 232 | def put(url: str, **kwargs: Unpack[ClientRequestParams]): 233 | return request(method="PUT", url=url, **kwargs) 234 | 235 | 236 | def patch(url: str, **kwargs: Unpack[ClientRequestParams]): 237 | return request(method="PATCH", url=url, **kwargs) 238 | -------------------------------------------------------------------------------- /tests/test_defs.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import pytest 4 | 5 | import certifi 6 | import primp # type: ignore 7 | 8 | 9 | def retry(max_retries=3, delay=1): 10 | def decorator(func): 11 | def wrapper(*args, **kwargs): 12 | for attempt in range(max_retries): 13 | try: 14 | return func(*args, **kwargs) 15 | except Exception as e: 16 | if attempt < max_retries - 1: 17 | sleep(delay) 18 | continue 19 | else: 20 | raise e 21 | 22 | return wrapper 23 | 24 | return decorator 25 | 26 | 27 | @retry() 28 | def test_request_get(): 29 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 30 | headers = {"X-Test": "test"} 31 | cookies = {"ccc": "ddd", "cccc": "dddd"} 32 | params = {"x": "aaa", "y": "bbb"} 33 | response = primp.request( 34 | "GET", 35 | "https://httpbin.org/anything", 36 | auth_bearer=auth_bearer, 37 | headers=headers, 38 | cookies=cookies, 39 | params=params, 40 | ca_cert_file=certifi.where(), 41 | ) 42 | assert response.status_code == 200 43 | assert response.headers["content-type"] == "application/json" 44 | json_data = response.json() 45 | assert json_data["method"] == "GET" 46 | assert json_data["headers"]["X-Test"] == "test" 47 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 48 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 49 | assert "Bearer bearerXXXXXXXXXXXXXXXXXXXX" in response.text 50 | assert b"Bearer bearerXXXXXXXXXXXXXXXXXXXX" in response.content 51 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 52 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 53 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 54 | 55 | 56 | @retry() 57 | def test_get(): 58 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 59 | headers = {"X-Test": "test"} 60 | cookies = {"ccc": "ddd", "cccc": "dddd"} 61 | params = {"x": "aaa", "y": "bbb"} 62 | response = primp.get( 63 | "https://httpbin.org/anything", 64 | auth_bearer=auth_bearer, 65 | headers=headers, 66 | cookies=cookies, 67 | params=params, 68 | ) 69 | assert response.status_code == 200 70 | json_data = response.json() 71 | assert json_data["method"] == "GET" 72 | assert json_data["headers"]["X-Test"] == "test" 73 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 74 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 75 | assert "Bearer bearerXXXXXXXXXXXXXXXXXXXX" in response.text 76 | assert b"Bearer bearerXXXXXXXXXXXXXXXXXXXX" in response.content 77 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 78 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 79 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 80 | 81 | 82 | @retry() 83 | def test_head(): 84 | response = primp.head("https://httpbin.org/anything", ca_cert_file=certifi.where()) 85 | assert response.status_code == 200 86 | assert "content-length" in response.headers 87 | 88 | 89 | @retry() 90 | def test_options(): 91 | response = primp.options( 92 | "https://httpbin.org/anything", ca_cert_file=certifi.where() 93 | ) 94 | assert response.status_code == 200 95 | assert sorted(response.headers["allow"].split(", ")) == [ 96 | "DELETE", 97 | "GET", 98 | "HEAD", 99 | "OPTIONS", 100 | "PATCH", 101 | "POST", 102 | "PUT", 103 | "TRACE", 104 | ] 105 | 106 | 107 | @retry() 108 | def test_delete(): 109 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 110 | headers = {"X-Test": "test"} 111 | cookies = {"ccc": "ddd", "cccc": "dddd"} 112 | params = {"x": "aaa", "y": "bbb"} 113 | response = primp.delete( 114 | "https://httpbin.org/anything", 115 | auth_bearer=auth_bearer, 116 | headers=headers, 117 | cookies=cookies, 118 | params=params, 119 | ) 120 | assert response.status_code == 200 121 | json_data = response.json() 122 | assert json_data["method"] == "DELETE" 123 | assert json_data["headers"]["X-Test"] == "test" 124 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 125 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 126 | assert "Bearer bearerXXXXXXXXXXXXXXXXXXXX" in response.text 127 | assert b"Bearer bearerXXXXXXXXXXXXXXXXXXXX" in response.content 128 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 129 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 130 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 131 | 132 | 133 | @retry() 134 | def test_post_content(): 135 | auth = ("user", "password") 136 | headers = {"X-Test": "test"} 137 | cookies = {"ccc": "ddd", "cccc": "dddd"} 138 | params = {"x": "aaa", "y": "bbb"} 139 | content = b"test content" 140 | response = primp.post( 141 | "https://httpbin.org/anything", 142 | auth=auth, 143 | headers=headers, 144 | cookies=cookies, 145 | params=params, 146 | content=content, 147 | ) 148 | assert response.status_code == 200 149 | json_data = response.json() 150 | assert json_data["method"] == "POST" 151 | assert json_data["headers"]["X-Test"] == "test" 152 | assert json_data["headers"]["Authorization"] == "Basic dXNlcjpwYXNzd29yZA==" 153 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 154 | assert json_data["data"] == "test content" 155 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 156 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 157 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 158 | 159 | 160 | @retry() 161 | def test_post_data(): 162 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 163 | headers = {"X-Test": "test"} 164 | cookies = {"ccc": "ddd", "cccc": "dddd"} 165 | params = {"x": "aaa", "y": "bbb"} 166 | data = {"key1": "value1", "key2": "value2"} 167 | response = primp.post( 168 | "https://httpbin.org/anything", 169 | auth_bearer=auth_bearer, 170 | headers=headers, 171 | cookies=cookies, 172 | params=params, 173 | data=data, 174 | ) 175 | assert response.status_code == 200 176 | json_data = response.json() 177 | assert json_data["method"] == "POST" 178 | assert json_data["headers"]["X-Test"] == "test" 179 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 180 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 181 | assert json_data["form"] == {"key1": "value1", "key2": "value2"} 182 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 183 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 184 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 185 | 186 | 187 | @retry() 188 | def test_post_json(): 189 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 190 | headers = {"X-Test": "test"} 191 | cookies = {"ccc": "ddd", "cccc": "dddd"} 192 | params = {"x": "aaa", "y": "bbb"} 193 | data = {"key1": "value1", "key2": "value2"} 194 | response = primp.post( 195 | "https://httpbin.org/anything", 196 | auth_bearer=auth_bearer, 197 | headers=headers, 198 | cookies=cookies, 199 | params=params, 200 | json=data, 201 | ) 202 | assert response.status_code == 200 203 | json_data = response.json() 204 | assert json_data["method"] == "POST" 205 | assert json_data["headers"]["X-Test"] == "test" 206 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 207 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 208 | assert json_data["json"] == data 209 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 210 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 211 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 212 | 213 | 214 | @pytest.fixture(scope="session") 215 | def test_files(tmp_path_factory): 216 | tmp_path_factory.mktemp("data") 217 | temp_file1 = tmp_path_factory.mktemp("data") / "img1.png" 218 | with open(temp_file1, "w") as f: 219 | f.write("aaa111") 220 | temp_file2 = tmp_path_factory.mktemp("data") / "img2.png" 221 | with open(temp_file2, "w") as f: 222 | f.write("bbb222") 223 | return str(temp_file1), str(temp_file2) 224 | 225 | 226 | def test_client_post_files(test_files): 227 | temp_file1, temp_file2 = test_files 228 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 229 | headers = {"X-Test": "test"} 230 | cookies = {"ccc": "ddd", "cccc": "dddd"} 231 | params = {"x": "aaa", "y": "bbb"} 232 | files = {"file1": temp_file1, "file2": temp_file2} 233 | response = primp.post( 234 | "https://httpbin.org/anything", 235 | auth_bearer=auth_bearer, 236 | headers=headers, 237 | cookies=cookies, 238 | params=params, 239 | files=files, 240 | ) 241 | assert response.status_code == 200 242 | json_data = response.json() 243 | assert json_data["method"] == "POST" 244 | assert json_data["headers"]["X-Test"] == "test" 245 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 246 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 247 | assert json_data["files"] == {"file1": "aaa111", "file2": "bbb222"} 248 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 249 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 250 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 251 | 252 | 253 | @retry() 254 | def test_patch(): 255 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 256 | headers = {"X-Test": "test"} 257 | cookies = {"ccc": "ddd", "cccc": "dddd"} 258 | params = {"x": "aaa", "y": "bbb"} 259 | data = {"key1": "value1", "key2": "value2"} 260 | response = primp.patch( 261 | "https://httpbin.org/anything", 262 | auth_bearer=auth_bearer, 263 | headers=headers, 264 | cookies=cookies, 265 | params=params, 266 | data=data, 267 | ) 268 | assert response.status_code == 200 269 | json_data = response.json() 270 | assert json_data["method"] == "PATCH" 271 | assert json_data["headers"]["X-Test"] == "test" 272 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 273 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 274 | assert json_data["form"] == {"key1": "value1", "key2": "value2"} 275 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 276 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 277 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 278 | 279 | 280 | @retry() 281 | def test_put(): 282 | auth_bearer = "bearerXXXXXXXXXXXXXXXXXXXX" 283 | headers = {"X-Test": "test"} 284 | cookies = {"ccc": "ddd", "cccc": "dddd"} 285 | params = {"x": "aaa", "y": "bbb"} 286 | data = {"key1": "value1", "key2": "value2"} 287 | response = primp.put( 288 | "https://httpbin.org/anything", 289 | auth_bearer=auth_bearer, 290 | headers=headers, 291 | cookies=cookies, 292 | params=params, 293 | data=data, 294 | ) 295 | assert response.status_code == 200 296 | json_data = response.json() 297 | assert json_data["method"] == "PUT" 298 | assert json_data["headers"]["X-Test"] == "test" 299 | assert json_data["headers"]["Authorization"] == "Bearer bearerXXXXXXXXXXXXXXXXXXXX" 300 | assert json_data["args"] == {"x": "aaa", "y": "bbb"} 301 | assert json_data["form"] == {"key1": "value1", "key2": "value2"} 302 | # assert json_data["headers"]["Cookie"] == "ccc=ddd; cccc=dddd" 303 | assert "ccc=ddd" in json_data["headers"]["Cookie"] 304 | assert "cccc=dddd" in json_data["headers"]["Cookie"] 305 | 306 | 307 | @retry() 308 | def test_get_impersonate_firefox133(): 309 | response = primp.get( 310 | "https://tls.peet.ws/api/all", 311 | #"https://tls.http.rw/api/all", 312 | impersonate="firefox_133", 313 | impersonate_os="linux", 314 | ) 315 | assert response.status_code == 200 316 | json_data = response.json() 317 | assert ( 318 | json_data["user_agent"] 319 | == "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0" 320 | ) 321 | assert json_data["tls"]["ja4"] == "t13d1716h2_5b57614c22b0_bed828528d07" 322 | assert ( 323 | json_data["http2"]["akamai_fingerprint_hash"] 324 | == "6ea73faa8fc5aac76bded7bd238f6433" 325 | ) 326 | assert json_data["tls"]["peetprint_hash"] == "199f9cf4a47bfc51995a9f3942190094" 327 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Python >= 3.8](https://img.shields.io/badge/python->=3.8-red.svg) [![](https://badgen.net/github/release/deedy5/pyreqwest-impersonate)](https://github.com/deedy5/pyreqwest-impersonate/releases) [![](https://badge.fury.io/py/primp.svg)](https://pypi.org/project/primp) [![Downloads](https://static.pepy.tech/badge/primp/week)](https://pepy.tech/project/primp) [![CI](https://github.com/deedy5/pyreqwest-impersonate/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/deedy5/pyreqwest-impersonate/actions/workflows/CI.yml) 2 | # 🪞PRIMP 3 | **🪞PRIMP** = **P**ython **R**equests **IMP**ersonate 4 | 5 | The fastest python HTTP client that can impersonate web browsers.
6 | Provides precompiled wheels:
7 | * 🐧 linux: `amd64`, `aarch64`, `armv7` (⚠️aarch64 and armv7 builds are `manylinux_2_34` compatible - `ubuntu>=22.04`, `debian>=12`);
8 | * 🐧 musllinux: `amd64`, `aarch64`;
9 | * 🪟 windows: `amd64`;
10 | * 🍏 macos: `amd64`, `aarch64`.
11 | 12 | ## Table of Contents 13 | 14 | - [Installation](#installation) 15 | - [Benchmark](#benchmark) 16 | - [Usage](#usage) 17 | - [I. Client](#i-client) 18 | - [Client methods](#client-methods) 19 | - [Response object](#response-object) 20 | - [Devices](#devices) 21 | - [Examples](#examples) 22 | - [II. AsyncClient](#ii-asyncclient) 23 | - [Disclaimer](#disclaimer) 24 | 25 | ## Installation 26 | 27 | ```python 28 | pip install -U primp 29 | ``` 30 | 31 | ## Benchmark 32 | 33 | ![](https://github.com/deedy5/primp/blob/main/benchmark.jpg?raw=true) 34 | 35 | ## Usage 36 | ### I. Client 37 | 38 | HTTP client that can impersonate web browsers. 39 | ```python 40 | class Client: 41 | """Initializes an HTTP client that can impersonate web browsers. 42 | 43 | Args: 44 | auth (tuple[str, str| None] | None): Username and password for basic authentication. Default is None. 45 | auth_bearer (str | None): Bearer token for authentication. Default is None. 46 | params (dict[str, str] | None): Default query parameters to include in all requests. Default is None. 47 | headers (dict[str, str] | None): Default headers to send with requests. If `impersonate` is set, this will be ignored. 48 | timeout (float | None): HTTP request timeout in seconds. Default is 30. 49 | cookie_store (bool | None): Enable a persistent cookie store. Received cookies will be preserved and included 50 | in additional requests. Default is True. 51 | referer (bool | None): Enable or disable automatic setting of the `Referer` header. Default is True. 52 | proxy (str | None): Proxy URL for HTTP requests. Example: "socks5://127.0.0.1:9150". Default is None. 53 | impersonate (str | None): Entity to impersonate. Example: "chrome_124". Default is None. 54 | Chrome: "chrome_100","chrome_101","chrome_104","chrome_105","chrome_106","chrome_107","chrome_108", 55 | "chrome_109","chrome_114","chrome_116","chrome_117","chrome_118","chrome_119","chrome_120", 56 | "chrome_123","chrome_124","chrome_126","chrome_127","chrome_128","chrome_129","chrome_130", 57 | "chrome_131","chrome_133" 58 | Safari: "safari_ios_16.5","safari_ios_17.2","safari_ios_17.4.1","safari_ios_18.1.1", 59 | "safari_15.3","safari_15.5","safari_15.6.1","safari_16","safari_16.5","safari_17.0", 60 | "safari_17.2.1","safari_17.4.1","safari_17.5","safari_18","safari_18.2","safari_ipad_18" 61 | OkHttp: "okhttp_3.9","okhttp_3.11","okhttp_3.13","okhttp_3.14","okhttp_4.9","okhttp_4.10","okhttp_5" 62 | Edge: "edge_101","edge_122","edge_127","edge_131" 63 | Firefox: "firefox_109","firefox_117","firefox_128","firefox_133","firefox_135" 64 | Select random: "random" 65 | impersonate_os (str | None): impersonate OS. Example: "windows". Default is "linux". 66 | Android: "android", iOS: "ios", Linux: "linux", Mac OS: "macos", Windows: "windows" 67 | Select random: "random" 68 | follow_redirects (bool | None): Whether to follow redirects. Default is True. 69 | max_redirects (int | None): Maximum redirects to follow. Default 20. Applies if `follow_redirects` is True. 70 | verify (bool | None): Verify SSL certificates. Default is True. 71 | ca_cert_file (str | None): Path to CA certificate store. Default is None. 72 | https_only` (bool | None): Restrict the Client to be used with HTTPS only requests. Default is `false`. 73 | http2_only` (bool | None): If true - use only HTTP/2; if false - use only HTTP/1. Default is `false`. 74 | 75 | """ 76 | ``` 77 | 78 | #### Client methods 79 | 80 | The `Client` class provides a set of methods for making HTTP requests: `get`, `head`, `options`, `delete`, `post`, `put`, `patch`, each of which internally utilizes the `request()` method for execution. The parameters for these methods closely resemble those in `httpx`. 81 | ```python 82 | def get( 83 | url: str, 84 | params: dict[str, str] | None = None, 85 | headers: dict[str, str] | None = None, 86 | cookies: dict[str, str] | None = None, 87 | auth: tuple[str, str| None] | None = None, 88 | auth_bearer: str | None = None, 89 | timeout: float | None = 30, 90 | ): 91 | """Performs a GET request to the specified URL. 92 | 93 | Args: 94 | url (str): The URL to which the request will be made. 95 | params (dict[str, str] | None): A map of query parameters to append to the URL. Default is None. 96 | headers (dict[str, str] | None): A map of HTTP headers to send with the request. Default is None. 97 | cookies (dict[str, str] | None): - An optional map of cookies to send with requests as the `Cookie` header. 98 | auth (tuple[str, str| None] | None): A tuple containing the username and an optional password 99 | for basic authentication. Default is None. 100 | auth_bearer (str | None): A string representing the bearer token for bearer token authentication. Default is None. 101 | timeout (float | None): The timeout for the request in seconds. Default is 30. 102 | 103 | """ 104 | ``` 105 | ```python 106 | def post( 107 | url: str, 108 | params: dict[str, str] | None = None, 109 | headers: dict[str, str] | None = None, 110 | cookies: dict[str, str] | None = None, 111 | content: bytes | None = None, 112 | data: dict[str, Any] | None = None, 113 | json: Any | None = None, 114 | files: dict[str, str] | None = None, 115 | auth: tuple[str, str| None] | None = None, 116 | auth_bearer: str | None = None, 117 | timeout: float | None = 30, 118 | ): 119 | """Performs a POST request to the specified URL. 120 | 121 | Args: 122 | url (str): The URL to which the request will be made. 123 | params (dict[str, str] | None): A map of query parameters to append to the URL. Default is None. 124 | headers (dict[str, str] | None): A map of HTTP headers to send with the request. Default is None. 125 | cookies (dict[str, str] | None): - An optional map of cookies to send with requests as the `Cookie` header. 126 | content (bytes | None): The content to send in the request body as bytes. Default is None. 127 | data (dict[str, Any] | None): The form data to send in the request body. Default is None. 128 | json (Any | None): A JSON serializable object to send in the request body. Default is None. 129 | files (dict[str, str] | None): A map of file fields to file paths to be sent as multipart/form-data. Default is None. 130 | auth (tuple[str, str| None] | None): A tuple containing the username and an optional password 131 | for basic authentication. Default is None. 132 | auth_bearer (str | None): A string representing the bearer token for bearer token authentication. Default is None. 133 | timeout (float | None): The timeout for the request in seconds. Default is 30. 134 | 135 | """ 136 | ``` 137 | #### Response object 138 | ```python 139 | resp.content 140 | resp.stream() # stream the response body in chunks of bytes 141 | resp.cookies 142 | resp.encoding 143 | resp.headers 144 | resp.json() 145 | resp.status_code 146 | resp.text 147 | resp.text_markdown # html is converted to markdown text 148 | resp.text_plain # html is converted to plain text 149 | resp.text_rich # html is converted to rich text 150 | resp.url 151 | ``` 152 | 153 | #### Devices 154 | 155 | ##### Impersonate 156 | 157 | - Chrome: `chrome_100`,`chrome_101`,`chrome_104`,`chrome_105`,`chrome_106`,`chrome_107`,`chrome_108`,`chrome_109`,`chrome_114`,`chrome_116`,`chrome_117`,`chrome_118`,`chrome_119`,`chrome_120`,`chrome_123`,`chrome_124`,`chrome_126`,`chrome_127`,`chrome_128`,`chrome_129`,`chrome_130`,`chrome_131`, `chrome_133` 158 | 159 | - Edge: `edge_101`,`edge_122`,`edge_127`, `edge_131` 160 | 161 | - Safari: `safari_ios_17.2`,`safari_ios_17.4.1`,`safari_ios_16.5`,`safari_ios_18.1.1`, `safari_15.3`,`safari_15.5`,`safari_15.6.1`,`safari_16`,`safari_16.5`,`safari_17.0`,`safari_17.2.1`,`safari_17.4.1`,`safari_17.5`,`safari_18`,`safari_18.2`, `safari_ipad_18` 162 | 163 | - OkHttp: `okhttp_3.9`,`okhttp_3.11`,`okhttp_3.13`,`okhttp_3.14`,`okhttp_4.9`,`okhttp_4.10`,`okhttp_5` 164 | 165 | - Firefox: `firefox_109`, `firefox_117`, `firefox_128`, `firefox_133`, `firefox_135` 166 | 167 | ##### Impersonate OS 168 | 169 | - `android`, `ios`, `linux`, `macos`, `windows` 170 | 171 | #### Examples 172 | 173 | ```python 174 | import primp 175 | 176 | # Impersonate 177 | client = primp.Client(impersonate="chrome_131", impersonate_os="windows") 178 | 179 | # Update headers 180 | headers = {"Referer": "https://cnn.com/"} 181 | client.headers_update(headers) 182 | 183 | # GET request 184 | resp = client.get("https://tls.peet.ws/api/all") 185 | 186 | # GET request with passing params and setting timeout 187 | params = {"param1": "value1", "param2": "value2"} 188 | resp = client.post(url="https://httpbin.org/anything", params=params, timeout=10) 189 | 190 | # Stream response 191 | resp = client.get("https://nytimes") 192 | for chunk in resp.stream(): 193 | print(chunk) 194 | 195 | # Cookies set 196 | cookies = {"c1_n": "c1_value", "c2_n": "c2_value"} 197 | client.set_cookies(url="https://nytimes.com", cookies) # set cookies for a specific domain 198 | client.get("https://nytimes.com/", cookies=cookies) # set cookies in request 199 | 200 | # Cookies get 201 | cookies = client.get_cookies(url="https://nytimes.com") # get cookies for a specific domain 202 | cookies = resp.cookies # get cookies from response 203 | 204 | # POST Binary Request Data 205 | content = b"some_data" 206 | resp = client.post(url="https://httpbin.org/anything", content=content) 207 | 208 | # POST Form Encoded Data 209 | data = {"key1": "value1", "key2": "value2"} 210 | resp = client.post(url="https://httpbin.org/anything", data=data) 211 | 212 | # POST JSON Encoded Data 213 | json = {"key1": "value1", "key2": "value2"} 214 | resp = client.post(url="https://httpbin.org/anything", json=json) 215 | 216 | # POST Multipart-Encoded Files 217 | files = {'file1': '/home/root/file1.txt', 'file2': 'home/root/file2.txt'} 218 | resp = client.post("https://httpbin.org/post", files=files) 219 | 220 | # Authentication using user/password 221 | resp = client.post(url="https://httpbin.org/anything", auth=("user", "password")) 222 | 223 | # Authentication using auth bearer 224 | resp = client.post(url="https://httpbin.org/anything", auth_bearer="bearerXXXXXXXXXXXXXXXXXXXX") 225 | 226 | # Using proxy or env var PRIMP_PROXY 227 | resp = primp.Client(proxy="http://127.0.0.1:8080") # set proxy in Client 228 | export PRIMP_PROXY="socks5://127.0.0.1:1080" # set proxy as environment variable 229 | 230 | # Using custom CA certificate store: 231 | #(!!!Primp already built with the Mozilla's latest trusted root certificates) 232 | resp = primp.Client(ca_cert_file="/cert/cacert.pem") 233 | resp = primp.Client(ca_cert_file=certifi.where()) 234 | export PRIMP_CA_BUNDLE="/home/user/Downloads/cert.pem" # set as environment variable 235 | 236 | # You can also use convenience functions that use a default Client instance under the hood: 237 | # primp.get() | primp.head() | primp.options() | primp.delete() | primp.post() | primp.patch() | primp.put() 238 | # These functions can accept the `impersonate` parameter: 239 | resp = primp.get("https://httpbin.org/anything", impersonate="chrome_131", impersonate_os="android") 240 | ``` 241 | 242 | ### II. AsyncClient 243 | 244 | `primp.AsyncClient()` is an asynchronous wrapper around the `primp.Client` class, offering the same functions, behavior, and input arguments. 245 | 246 | ```python3 247 | import asyncio 248 | import logging 249 | 250 | import primp 251 | 252 | async def aget_text(url): 253 | async with primp.AsyncClient(impersonate="chrome_131") as client: 254 | resp = await client.get(url) 255 | return resp.text 256 | 257 | async def main(): 258 | urls = ["https://nytimes.com/", "https://cnn.com/", "https://abcnews.go.com/"] 259 | tasks = [aget_text(u) for u in urls] 260 | results = await asyncio.gather(*tasks) 261 | 262 | if __name__ == "__main__": 263 | logging.basicConfig(level=logging.INFO) 264 | asyncio.run(main()) 265 | ``` 266 | 267 | ## Disclaimer 268 | 269 | This tool is for educational purposes only. Use it at your own risk. 270 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v1.7.4 2 | # To update, run 3 | # 4 | # maturin generate-ci github -o .github/workflows/CI.yml --pytest 5 | # 6 | name: CI 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | - '*' 13 | tags: 14 | - '*' 15 | pull_request: 16 | workflow_dispatch: 17 | 18 | permissions: 19 | contents: read 20 | 21 | jobs: 22 | linux: 23 | runs-on: ${{ matrix.platform.runner }} 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | platform: 28 | - runner: ubuntu-latest 29 | target: x86_64 30 | apt_packages: '' 31 | custom_env: {} 32 | - runner: ubuntu-latest 33 | target: aarch64 34 | apt_packages: crossbuild-essential-arm64 35 | custom_env: 36 | CFLAGS_aarch64_unknown_linux_gnu: -D__ARM_ARCH=8 37 | CC: aarch64-linux-gnu-gcc 38 | CXX: aarch64-linux-gnu-g++ 39 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-g++ 40 | - runner: ubuntu-latest 41 | target: armv7 42 | apt_packages: crossbuild-essential-armhf 43 | custom_env: 44 | CC: arm-linux-gnueabihf-gcc 45 | CXX: arm-linux-gnueabihf-g++ 46 | CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-g++ 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: actions/setup-python@v5 50 | with: 51 | python-version: 3.x 52 | - name: Install target-specific APT dependencies 53 | if: "matrix.platform.apt_packages != ''" 54 | run: sudo apt-get update && sudo apt-get install -y ${{ matrix.platform.apt_packages }} 55 | - name: Build wheels 56 | uses: PyO3/maturin-action@v1 57 | with: 58 | rust-toolchain: stable 59 | target: ${{ matrix.platform.target }} 60 | args: ${{ matrix.platform.target == 'x86_64' && '--release --out dist --zig' || '--release --out dist' }} 61 | sccache: 'false' 62 | manylinux: auto 63 | container: 'off' 64 | env: ${{ matrix.platform.custom_env }} 65 | - name: Upload wheels 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: wheels-linux-${{ matrix.platform.target }} 69 | path: dist 70 | - name: pytest x86_64 71 | if: ${{ startsWith(matrix.platform.target, 'x86_64') }} 72 | shell: bash 73 | run: | 74 | set -e 75 | pip install uv 76 | for version in 3.8 3.9 3.10 3.11 3.12 3.13; do 77 | uv venv --preview --python $version 78 | source .venv/bin/activate 79 | uv pip install certifi pytest pytest-asyncio typing_extensions 80 | uv pip install primp --no-index --find-links dist --force-reinstall 81 | pytest 82 | done 83 | - name: pytest aarch64 84 | if: ${{ startsWith(matrix.platform.target, 'aarch64') }} 85 | uses: uraimo/run-on-arch-action@v2 86 | with: 87 | arch: ${{ matrix.platform.target }} 88 | distro: ubuntu22.04 89 | githubToken: ${{ github.token }} 90 | install: | 91 | apt-get update 92 | apt-get install -y --no-install-recommends python3 python3-pip 93 | run: | 94 | set -e 95 | pip install uv 96 | for version in 3.8 3.9 3.10 3.11 3.12 3.13; do 97 | uv venv --preview --python $version 98 | source .venv/bin/activate 99 | uv pip install certifi pytest pytest-asyncio typing_extensions 100 | uv pip install primp --no-index --find-links dist --force-reinstall 101 | pytest 102 | done 103 | - name: pytest armv7 104 | if: ${{ startsWith(matrix.platform.target, 'armv7') }} 105 | uses: uraimo/run-on-arch-action@v2 106 | with: 107 | arch: ${{ matrix.platform.target }} 108 | distro: ubuntu22.04 109 | githubToken: ${{ github.token }} 110 | install: | 111 | apt-get update 112 | apt-get install -y --no-install-recommends python3 python3-pip python3-venv 113 | run: | 114 | set -e 115 | python3 -m venv .venv 116 | source .venv/bin/activate 117 | pip install certifi pytest pytest-asyncio typing_extensions 118 | pip install primp --no-index --find-links dist --force-reinstall 119 | pytest 120 | 121 | musllinux: 122 | runs-on: ${{ matrix.platform.runner }} 123 | strategy: 124 | fail-fast: false 125 | matrix: 126 | platform: 127 | - runner: ubuntu-latest 128 | target: x86_64 129 | package: x86_64-linux-musl-cross 130 | apt_packages: '' 131 | custom_env: 132 | CC: x86_64-linux-musl-gcc 133 | CXX: x86_64-linux-musl-gcc 134 | CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: x86_64-linux-musl-g++ 135 | - runner: ubuntu-latest 136 | target: aarch64 137 | package: aarch64-linux-musl-cross 138 | apt_packages: crossbuild-essential-arm64 139 | custom_env: 140 | CC: aarch64-linux-musl-gcc 141 | CXX: aarch64-linux-musl-gcc 142 | CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER: aarch64-linux-musl-g++ 143 | #- runner: ubuntu-latest 144 | # target: armv7 145 | # package: armv7l-linux-musleabihf-cross 146 | # apt_packages: crossbuild-essential-armhf 147 | # custom_env: 148 | # CC: armv7l-linux-musleabihf-gcc 149 | # CXX: armv7l-linux-musleabihf-g++ 150 | # CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: armv7l-linux-musleabihf-g++ 151 | steps: 152 | - uses: actions/checkout@v4 153 | - uses: actions/setup-python@v5 154 | with: 155 | python-version: 3.x 156 | - name: Install target-specific APT dependencies 157 | if: "matrix.platform.apt_packages != ''" 158 | run: sudo apt-get update && sudo apt-get install -y ${{ matrix.platform.apt_packages }} 159 | - name: Prepare musl cross-compiler 160 | run: | 161 | curl -O http://musl.cc/${{ matrix.platform.package }}.tgz 162 | tar xzf ${{ matrix.platform.package }}.tgz -C /opt 163 | echo "/opt/${{ matrix.platform.package }}/bin/" >> $GITHUB_PATH 164 | - name: Build wheels 165 | uses: PyO3/maturin-action@v1 166 | with: 167 | rust-toolchain: stable 168 | target: ${{ matrix.platform.target }} 169 | args: --release --out dist 170 | sccache: 'true' 171 | manylinux: musllinux_1_2 172 | container: 'off' 173 | env: ${{ matrix.platform.custom_env }} 174 | - name: Upload wheels 175 | uses: actions/upload-artifact@v4 176 | with: 177 | name: wheels-musllinux-${{ matrix.platform.target }} 178 | path: dist 179 | - name: QEMU 180 | if: matrix.platform.target != 'x86_64' 181 | uses: docker/setup-qemu-action@v3 182 | - name: pytest 183 | uses: addnab/docker-run-action@v3 184 | with: 185 | image: quay.io/pypa/musllinux_1_2_${{ matrix.platform.target }}:latest 186 | options: -v ${{ github.workspace }}:/io -w /io 187 | run: | 188 | for version in 3.8 3.9 3.10 3.11 3.12 3.13; do 189 | python$version -m venv .venv 190 | source .venv/bin/activate 191 | pip install certifi pytest pytest-asyncio typing_extensions 192 | pip install primp --no-index --find-links dist --force-reinstall 193 | pytest 194 | done 195 | 196 | windows: 197 | runs-on: ${{ matrix.platform.runner }} 198 | strategy: 199 | fail-fast: false 200 | matrix: 201 | platform: 202 | - runner: windows-latest 203 | target: x64 204 | steps: 205 | - uses: actions/checkout@v4 206 | - uses: actions/setup-python@v5 207 | with: 208 | python-version: 3.x 209 | architecture: ${{ matrix.platform.target }} 210 | - name: Install nasm 211 | run: choco install nasm 212 | - name: Build wheels 213 | uses: PyO3/maturin-action@v1 214 | with: 215 | rust-toolchain: stable 216 | target: ${{ matrix.platform.target }} 217 | args: --release --out dist 218 | sccache: 'true' 219 | - name: Upload wheels 220 | uses: actions/upload-artifact@v4 221 | with: 222 | name: wheels-windows-${{ matrix.platform.target }} 223 | path: dist 224 | - name: pytest 225 | if: ${{ !startsWith(matrix.platform.target, 'aarch64') }} 226 | shell: bash 227 | run: | 228 | set -e 229 | pip install uv 230 | for version in 3.8 3.9 3.10 3.11 3.12 3.13; do 231 | uv venv --preview --python $version 232 | source .venv/Scripts/activate 233 | uv pip install certifi pytest pytest-asyncio typing_extensions 234 | uv pip install primp --no-index --find-links dist --force-reinstall 235 | pytest 236 | done 237 | 238 | macos: 239 | runs-on: ${{ matrix.platform.runner }} 240 | strategy: 241 | fail-fast: false 242 | matrix: 243 | platform: 244 | - runner: macos-13 245 | target: x86_64 246 | - runner: macos-14 247 | target: aarch64 248 | steps: 249 | - uses: actions/checkout@v4 250 | - uses: actions/setup-python@v5 251 | with: 252 | python-version: 3.x 253 | - name: Build wheels 254 | uses: PyO3/maturin-action@v1 255 | with: 256 | rust-toolchain: stable 257 | target: ${{ matrix.platform.target }} 258 | args: --release --out dist 259 | sccache: 'true' 260 | - name: Upload wheels 261 | uses: actions/upload-artifact@v4 262 | with: 263 | name: wheels-macos-${{ matrix.platform.target }} 264 | path: dist 265 | - name: pytest 266 | run: | 267 | set -e 268 | pip install uv 269 | for version in 3.8 3.9 3.10 3.11 3.12 3.13; do 270 | uv venv --preview --python $version 271 | source .venv/bin/activate 272 | uv pip install certifi pytest pytest-asyncio typing_extensions 273 | uv pip install primp --no-index --find-links dist --force-reinstall 274 | pytest 275 | done 276 | 277 | sdist: 278 | runs-on: ubuntu-latest 279 | steps: 280 | - uses: actions/checkout@v4 281 | - name: Build sdist 282 | uses: PyO3/maturin-action@v1 283 | with: 284 | command: sdist 285 | args: --out dist 286 | - name: Upload sdist 287 | uses: actions/upload-artifact@v4 288 | with: 289 | name: wheels-sdist 290 | path: dist 291 | 292 | release: 293 | name: Release 294 | runs-on: ubuntu-latest 295 | if: "startsWith(github.ref, 'refs/tags/')" 296 | needs: [linux, musllinux, windows, macos, sdist] 297 | permissions: 298 | # Use to sign the release artifacts 299 | id-token: write 300 | # Used to upload release artifacts 301 | contents: write 302 | # Used to generate artifact attestation 303 | attestations: write 304 | steps: 305 | - uses: actions/download-artifact@v4 306 | - name: Generate artifact attestation 307 | uses: actions/attest-build-provenance@v1 308 | with: 309 | subject-path: 'wheels-*/*' 310 | - name: Publish to PyPI 311 | uses: PyO3/maturin-action@v1 312 | env: 313 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 314 | with: 315 | command: upload 316 | args: --non-interactive --skip-existing wheels-*/* 317 | 318 | benchmark: 319 | permissions: 320 | contents: write 321 | runs-on: ubuntu-latest 322 | needs: [linux] 323 | steps: 324 | - uses: actions/checkout@v4 325 | - name: Set up Python 326 | uses: actions/setup-python@v5 327 | with: 328 | python-version: 3.x 329 | - name: Download wheels 330 | uses: actions/download-artifact@v4 331 | with: 332 | name: wheels-linux-x86_64 333 | - name: Install dependencies 334 | run: | 335 | pip install -r benchmark/requirements.txt 336 | pip install primp --no-index --find-links ./ --force-reinstall 337 | - name: Start Uvicorn server 338 | run: | 339 | uvicorn benchmark.server:app --host 0.0.0.0 --port 8000 & 340 | sleep 10 341 | - name: Run benchmark 342 | run: python benchmark/benchmark.py 343 | - name: Generate image, commit to the temp branch, merge changes into main, delete temp branch 344 | if: "startsWith(github.ref, 'refs/tags/')" 345 | run: | 346 | python benchmark/generate_image.py 347 | git config --global user.name 'GitHub Actions' 348 | git config --global user.email 'actions@github.com' 349 | git add \*.jpg 350 | git diff --quiet && git diff --staged --quiet || git commit -m "Update generated image" 351 | git checkout -b update-generated-image 352 | git push https://${{ secrets.PUSH_TOKEN }}@github.com/deedy5/primp.git update-generated-image || echo "No changes to push" 353 | git fetch origin 354 | git checkout main 355 | git merge update-generated-image 356 | git push https://${{ secrets.PUSH_TOKEN }}@github.com/deedy5/primp.git main 357 | git push origin --delete update-generated-image 358 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_arguments)] 2 | use std::sync::{Arc, LazyLock, Mutex}; 3 | use std::time::Duration; 4 | 5 | use anyhow::Result; 6 | use foldhash::fast::RandomState; 7 | use indexmap::IndexMap; 8 | use pyo3::prelude::*; 9 | use pythonize::depythonize; 10 | use rquest::{ 11 | header::{HeaderValue, COOKIE}, 12 | multipart, 13 | redirect::Policy, 14 | Body, Impersonate, ImpersonateOS, Method, 15 | }; 16 | use serde_json::Value; 17 | use tokio::{ 18 | fs::File, 19 | runtime::{self, Runtime}, 20 | }; 21 | use tokio_util::codec::{BytesCodec, FramedRead}; 22 | use tracing; 23 | 24 | mod impersonate; 25 | use impersonate::{ImpersonateFromStr, ImpersonateOSFromStr}; 26 | mod response; 27 | use response::Response; 28 | 29 | mod traits; 30 | use traits::HeadersTraits; 31 | 32 | mod utils; 33 | use utils::load_ca_certs; 34 | 35 | type IndexMapSSR = IndexMap; 36 | 37 | // Tokio global one-thread runtime 38 | static RUNTIME: LazyLock = LazyLock::new(|| { 39 | runtime::Builder::new_current_thread() 40 | .enable_all() 41 | .build() 42 | .unwrap() 43 | }); 44 | 45 | #[pyclass(subclass)] 46 | /// HTTP client that can impersonate web browsers. 47 | pub struct RClient { 48 | client: Arc>, 49 | #[pyo3(get, set)] 50 | auth: Option<(String, Option)>, 51 | #[pyo3(get, set)] 52 | auth_bearer: Option, 53 | #[pyo3(get, set)] 54 | params: Option, 55 | #[pyo3(get, set)] 56 | proxy: Option, 57 | #[pyo3(get, set)] 58 | timeout: Option, 59 | #[pyo3(get)] 60 | impersonate: Option, 61 | #[pyo3(get)] 62 | impersonate_os: Option, 63 | } 64 | 65 | #[pymethods] 66 | impl RClient { 67 | /// Initializes an HTTP client that can impersonate web browsers. 68 | /// 69 | /// This function creates a new HTTP client instance that can impersonate various web browsers. 70 | /// It allows for customization of headers, proxy settings, timeout, impersonation type, SSL certificate verification, 71 | /// and HTTP version preferences. 72 | /// 73 | /// # Arguments 74 | /// 75 | /// * `auth` - A tuple containing the username and an optional password for basic authentication. Default is None. 76 | /// * `auth_bearer` - A string representing the bearer token for bearer token authentication. Default is None. 77 | /// * `params` - A map of query parameters to append to the URL. Default is None. 78 | /// * `headers` - An optional map of HTTP headers to send with requests. If `impersonate` is set, this will be ignored. 79 | /// * `cookie_store` - Enable a persistent cookie store. Received cookies will be preserved and included 80 | /// in additional requests. Default is `true`. 81 | /// * `referer` - Enable or disable automatic setting of the `Referer` header. Default is `true`. 82 | /// * `proxy` - An optional proxy URL for HTTP requests. 83 | /// * `timeout` - An optional timeout for HTTP requests in seconds. 84 | /// * `impersonate` - An optional entity to impersonate. Supported browsers and versions include Chrome, Safari, OkHttp, and Edge. 85 | /// * `impersonate_os` - An optional entity to impersonate OS. Supported OS: android, ios, linux, macos, windows. 86 | /// * `follow_redirects` - A boolean to enable or disable following redirects. Default is `true`. 87 | /// * `max_redirects` - The maximum number of redirects to follow. Default is 20. Applies if `follow_redirects` is `true`. 88 | /// * `verify` - An optional boolean indicating whether to verify SSL certificates. Default is `true`. 89 | /// * `ca_cert_file` - Path to CA certificate store. Default is None. 90 | /// * `https_only` - Restrict the Client to be used with HTTPS only requests. Default is `false`. 91 | /// * `http2_only` - If true - use only HTTP/2, if false - use only HTTP/1. Default is `false`. 92 | /// 93 | /// # Example 94 | /// 95 | /// ``` 96 | /// from primp import Client 97 | /// 98 | /// client = Client( 99 | /// auth=("name", "password"), 100 | /// params={"p1k": "p1v", "p2k": "p2v"}, 101 | /// headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"}, 102 | /// cookie_store=False, 103 | /// referer=False, 104 | /// proxy="http://127.0.0.1:8080", 105 | /// timeout=10, 106 | /// impersonate="chrome_123", 107 | /// impersonate_os="windows", 108 | /// follow_redirects=True, 109 | /// max_redirects=1, 110 | /// verify=True, 111 | /// ca_cert_file="/cert/cacert.pem", 112 | /// https_only=True, 113 | /// http2_only=True, 114 | /// ) 115 | /// ``` 116 | #[new] 117 | #[pyo3(signature = (auth=None, auth_bearer=None, params=None, headers=None, cookie_store=true, 118 | referer=true, proxy=None, timeout=None, impersonate=None, impersonate_os=None, follow_redirects=true, 119 | max_redirects=20, verify=true, ca_cert_file=None, https_only=false, http2_only=false))] 120 | fn new( 121 | auth: Option<(String, Option)>, 122 | auth_bearer: Option, 123 | params: Option, 124 | headers: Option, 125 | cookie_store: Option, 126 | referer: Option, 127 | proxy: Option, 128 | timeout: Option, 129 | impersonate: Option, 130 | impersonate_os: Option, 131 | follow_redirects: Option, 132 | max_redirects: Option, 133 | verify: Option, 134 | ca_cert_file: Option, 135 | https_only: Option, 136 | http2_only: Option, 137 | ) -> Result { 138 | // Client builder 139 | let mut client_builder = rquest::Client::builder(); 140 | 141 | // Impersonate 142 | if let Some(impersonate) = &impersonate { 143 | let imp = Impersonate::from_str(&impersonate.as_str())?; 144 | let imp_os = if let Some(impersonate_os) = &impersonate_os { 145 | ImpersonateOS::from_str(&impersonate_os.as_str())? 146 | } else { 147 | ImpersonateOS::default() 148 | }; 149 | let impersonate_builder = Impersonate::builder() 150 | .impersonate(imp) 151 | .impersonate_os(imp_os) 152 | .build(); 153 | client_builder = client_builder.impersonate(impersonate_builder); 154 | } 155 | 156 | // Headers 157 | if let Some(headers) = headers { 158 | let headers_headermap = headers.to_headermap(); 159 | client_builder = client_builder.default_headers(headers_headermap); 160 | }; 161 | 162 | // Cookie_store 163 | if cookie_store.unwrap_or(true) { 164 | client_builder = client_builder.cookie_store(true); 165 | } 166 | 167 | // Referer 168 | if referer.unwrap_or(true) { 169 | client_builder = client_builder.referer(true); 170 | } 171 | 172 | // Proxy 173 | let proxy = proxy.or_else(|| std::env::var("PRIMP_PROXY").ok()); 174 | if let Some(proxy) = &proxy { 175 | client_builder = client_builder.proxy(rquest::Proxy::all(proxy)?); 176 | } 177 | 178 | // Timeout 179 | if let Some(seconds) = timeout { 180 | client_builder = client_builder.timeout(Duration::from_secs_f64(seconds)); 181 | } 182 | 183 | // Redirects 184 | if follow_redirects.unwrap_or(true) { 185 | client_builder = client_builder.redirect(Policy::limited(max_redirects.unwrap_or(20))); 186 | } else { 187 | client_builder = client_builder.redirect(Policy::none()); 188 | } 189 | 190 | // Ca_cert_file. BEFORE!!! verify (fn load_ca_certs() reads env var PRIMP_CA_BUNDLE) 191 | if let Some(ca_bundle_path) = &ca_cert_file { 192 | std::env::set_var("PRIMP_CA_BUNDLE", ca_bundle_path); 193 | } 194 | 195 | // Verify 196 | if verify.unwrap_or(true) { 197 | client_builder = client_builder.root_cert_store(load_ca_certs); 198 | } else { 199 | client_builder = client_builder.danger_accept_invalid_certs(true); 200 | } 201 | 202 | // Https_only 203 | if let Some(true) = https_only { 204 | client_builder = client_builder.https_only(true); 205 | } 206 | 207 | // Http2_only 208 | if let Some(true) = http2_only { 209 | client_builder = client_builder.http2_only(); 210 | } 211 | 212 | let client = Arc::new(Mutex::new(client_builder.build()?)); 213 | 214 | Ok(RClient { 215 | client, 216 | auth, 217 | auth_bearer, 218 | params, 219 | proxy, 220 | timeout, 221 | impersonate, 222 | impersonate_os, 223 | }) 224 | } 225 | 226 | #[getter] 227 | pub fn get_headers(&self) -> Result { 228 | let client = self.client.lock().unwrap(); 229 | let mut headers = client.headers().clone(); 230 | headers.remove(COOKIE); 231 | Ok(headers.to_indexmap()) 232 | } 233 | 234 | #[setter] 235 | pub fn set_headers(&self, new_headers: Option) -> Result<()> { 236 | let mut client = self.client.lock().unwrap(); 237 | let mut mclient = client.as_mut(); 238 | let headers = mclient.headers(); 239 | headers.clear(); 240 | if let Some(new_headers) = new_headers { 241 | for (k, v) in new_headers { 242 | headers.insert_key_value(k, v)? 243 | } 244 | } 245 | Ok(()) 246 | } 247 | 248 | pub fn headers_update(&self, new_headers: Option) -> Result<()> { 249 | let mut client = self.client.lock().unwrap(); 250 | let mut mclient = client.as_mut(); 251 | let headers = mclient.headers(); 252 | if let Some(new_headers) = new_headers { 253 | for (k, v) in new_headers { 254 | headers.insert_key_value(k, v)? 255 | } 256 | } 257 | Ok(()) 258 | } 259 | 260 | #[getter] 261 | pub fn get_proxy(&self) -> Result> { 262 | Ok(self.proxy.to_owned()) 263 | } 264 | 265 | #[setter] 266 | pub fn set_proxy(&mut self, proxy: String) -> Result<()> { 267 | let mut client = self.client.lock().unwrap(); 268 | let rproxy = rquest::Proxy::all(proxy.clone())?; 269 | client.as_mut().proxies(vec![rproxy]); 270 | self.proxy = Some(proxy); 271 | Ok(()) 272 | } 273 | 274 | #[setter] 275 | pub fn set_impersonate(&mut self, impersonate: String) -> Result<()> { 276 | let mut client = self.client.lock().unwrap(); 277 | let imp = Impersonate::from_str(&impersonate.as_str())?; 278 | let imp_os = if let Some(impersonate_os) = &self.impersonate_os { 279 | ImpersonateOS::from_str(&impersonate_os.as_str())? 280 | } else { 281 | ImpersonateOS::default() 282 | }; 283 | let impersonate_builder = Impersonate::builder() 284 | .impersonate(imp) 285 | .impersonate_os(imp_os) 286 | .build(); 287 | client.as_mut().impersonate(impersonate_builder); 288 | self.impersonate = Some(impersonate); 289 | Ok(()) 290 | } 291 | 292 | #[setter] 293 | pub fn set_impersonate_os(&mut self, impersonate_os: String) -> Result<()> { 294 | let mut client = self.client.lock().unwrap(); 295 | let imp_os = ImpersonateOS::from_str(&impersonate_os.as_str())?; 296 | let mut impersonate_builder = Impersonate::builder().impersonate_os(imp_os); 297 | if let Some(impersonate) = &self.impersonate { 298 | let imp = Impersonate::from_str(&impersonate.as_str())?; 299 | impersonate_builder = impersonate_builder.impersonate(imp); 300 | } 301 | client.as_mut().impersonate(impersonate_builder.build()); 302 | self.impersonate_os = Some(impersonate_os); 303 | Ok(()) 304 | } 305 | 306 | #[pyo3(signature = (url))] 307 | fn get_cookies(&self, url: &str) -> Result { 308 | let url = rquest::Url::parse(url).expect("Error parsing URL: {:url}"); 309 | let client = self.client.lock().unwrap(); 310 | let cookie = client.get_cookies(&url).expect("No cookies found"); 311 | let cookie_str = cookie.to_str()?; 312 | let mut cookie_map = IndexMap::with_capacity_and_hasher(10, RandomState::default()); 313 | for cookie in cookie_str.split(';') { 314 | let mut parts = cookie.splitn(2, '='); 315 | if let (Some(key), Some(value)) = (parts.next(), parts.next()) { 316 | cookie_map.insert(key.trim().to_string(), value.trim().to_string()); 317 | } 318 | } 319 | Ok(cookie_map) 320 | } 321 | 322 | #[pyo3(signature = (url, cookies))] 323 | fn set_cookies(&self, url: &str, cookies: Option) -> Result<()> { 324 | let url = rquest::Url::parse(url).expect("Error parsing URL: {:url}"); 325 | if let Some(cookies) = cookies { 326 | let header_values: Vec = cookies 327 | .iter() 328 | .filter_map(|(key, value)| { 329 | HeaderValue::from_str(&format!("{}={}", key, value)).ok() 330 | }) 331 | .collect(); 332 | let client = self.client.lock().unwrap(); 333 | client.set_cookies(&url, header_values); 334 | } 335 | Ok(()) 336 | } 337 | 338 | /// Constructs an HTTP request with the given method, URL, and optionally sets a timeout, headers, and query parameters. 339 | /// Sends the request and returns a `Response` object containing the server's response. 340 | /// 341 | /// # Arguments 342 | /// 343 | /// * `method` - The HTTP method to use (e.g., "GET", "POST"). 344 | /// * `url` - The URL to which the request will be made. 345 | /// * `params` - A map of query parameters to append to the URL. Default is None. 346 | /// * `headers` - A map of HTTP headers to send with the request. Default is None. 347 | /// * `cookies` - An optional map of cookies to send with requests as the `Cookie` header. 348 | /// * `content` - The content to send in the request body as bytes. Default is None. 349 | /// * `data` - The form data to send in the request body. Default is None. 350 | /// * `json` - A JSON serializable object to send in the request body. Default is None. 351 | /// * `files` - A map of file fields to file paths to be sent as multipart/form-data. Default is None. 352 | /// * `auth` - A tuple containing the username and an optional password for basic authentication. Default is None. 353 | /// * `auth_bearer` - A string representing the bearer token for bearer token authentication. Default is None. 354 | /// * `timeout` - The timeout for the request in seconds. Default is 30. 355 | /// 356 | /// # Returns 357 | /// 358 | /// * `Response` - A response object containing the server's response to the request. 359 | /// 360 | /// # Errors 361 | /// 362 | /// * `PyException` - If there is an error making the request. 363 | #[pyo3(signature = (method, url, params=None, headers=None, cookies=None, content=None, 364 | data=None, json=None, files=None, auth=None, auth_bearer=None, timeout=None))] 365 | fn request( 366 | &self, 367 | py: Python, 368 | method: &str, 369 | url: &str, 370 | params: Option, 371 | headers: Option, 372 | cookies: Option, 373 | content: Option>, 374 | data: Option<&Bound<'_, PyAny>>, 375 | json: Option<&Bound<'_, PyAny>>, 376 | files: Option>, 377 | auth: Option<(String, Option)>, 378 | auth_bearer: Option, 379 | timeout: Option, 380 | ) -> Result { 381 | let client = Arc::clone(&self.client); 382 | let method = Method::from_bytes(method.as_bytes())?; 383 | let is_post_put_patch = matches!(method, Method::POST | Method::PUT | Method::PATCH); 384 | let params = params.or_else(|| self.params.clone()); 385 | let data_value: Option = data.map(depythonize).transpose()?; 386 | let json_value: Option = json.map(depythonize).transpose()?; 387 | let auth = auth.or(self.auth.clone()); 388 | let auth_bearer = auth_bearer.or(self.auth_bearer.clone()); 389 | let timeout: Option = timeout.or(self.timeout); 390 | 391 | // Cookies 392 | if let Some(cookies) = cookies { 393 | let url = rquest::Url::parse(url)?; 394 | let cookie_values: Vec = cookies 395 | .iter() 396 | .filter_map(|(key, value)| { 397 | let cookie_string = format!("{}={}", key, value.to_string()); 398 | HeaderValue::from_str(&cookie_string).ok() 399 | }) 400 | .collect(); 401 | let client = client.lock().unwrap(); 402 | client.set_cookies(&url, cookie_values); 403 | } 404 | 405 | let future = async { 406 | // Create request builder 407 | let mut request_builder = client.lock().unwrap().request(method, url); 408 | 409 | // Params 410 | if let Some(params) = params { 411 | request_builder = request_builder.query(¶ms); 412 | } 413 | 414 | // Headers 415 | if let Some(headers) = headers { 416 | request_builder = request_builder.headers(headers.to_headermap()); 417 | } 418 | 419 | // Only if method POST || PUT || PATCH 420 | if is_post_put_patch { 421 | // Content 422 | if let Some(content) = content { 423 | request_builder = request_builder.body(content); 424 | } 425 | // Data 426 | if let Some(form_data) = data_value { 427 | request_builder = request_builder.form(&form_data); 428 | } 429 | // Json 430 | if let Some(json_data) = json_value { 431 | request_builder = request_builder.json(&json_data); 432 | } 433 | // Files 434 | if let Some(files) = files { 435 | let mut form = multipart::Form::new(); 436 | for (file_name, file_path) in files { 437 | let file = File::open(file_path).await?; 438 | let stream = FramedRead::new(file, BytesCodec::new()); 439 | let file_body = Body::wrap_stream(stream); 440 | let part = multipart::Part::stream(file_body).file_name(file_name.clone()); 441 | form = form.part(file_name, part); 442 | } 443 | request_builder = request_builder.multipart(form); 444 | } 445 | } 446 | 447 | // Auth 448 | if let Some((username, password)) = auth { 449 | request_builder = request_builder.basic_auth(username, password); 450 | } else if let Some(token) = auth_bearer { 451 | request_builder = request_builder.bearer_auth(token); 452 | } 453 | 454 | // Timeout 455 | if let Some(seconds) = timeout { 456 | request_builder = request_builder.timeout(Duration::from_secs_f64(seconds)); 457 | } 458 | 459 | // Send the request and await the response 460 | let resp: rquest::Response = request_builder.send().await?; 461 | let url: String = resp.url().to_string(); 462 | let status_code = resp.status().as_u16(); 463 | 464 | tracing::info!("response: {} {}", url, status_code); 465 | Ok((resp, url, status_code)) 466 | }; 467 | 468 | // Execute an async future, releasing the Python GIL for concurrency. 469 | // Use Tokio global runtime to block on the future. 470 | let response: Result<(rquest::Response, String, u16)> = 471 | py.allow_threads(|| RUNTIME.block_on(future)); 472 | let result = response?; 473 | let resp = http::Response::from(result.0); 474 | let url = result.1; 475 | let status_code = result.2; 476 | Ok(Response { 477 | resp, 478 | _content: None, 479 | _encoding: None, 480 | _headers: None, 481 | _cookies: None, 482 | url, 483 | status_code, 484 | }) 485 | } 486 | } 487 | 488 | #[pymodule] 489 | fn primp(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 490 | pyo3_log::init(); 491 | 492 | m.add_class::()?; 493 | Ok(()) 494 | } 495 | -------------------------------------------------------------------------------- /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.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "alloc-no-stdlib" 31 | version = "2.0.4" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 34 | 35 | [[package]] 36 | name = "alloc-stdlib" 37 | version = "0.2.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 40 | dependencies = [ 41 | "alloc-no-stdlib", 42 | ] 43 | 44 | [[package]] 45 | name = "antidote" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5" 49 | 50 | [[package]] 51 | name = "anyhow" 52 | version = "1.0.98" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 55 | 56 | [[package]] 57 | name = "arc-swap" 58 | version = "1.7.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 61 | 62 | [[package]] 63 | name = "async-compression" 64 | version = "0.4.22" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" 67 | dependencies = [ 68 | "brotli", 69 | "flate2", 70 | "futures-core", 71 | "memchr", 72 | "pin-project-lite", 73 | "tokio", 74 | "zstd", 75 | "zstd-safe", 76 | ] 77 | 78 | [[package]] 79 | name = "atomic-waker" 80 | version = "1.1.2" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 83 | 84 | [[package]] 85 | name = "autocfg" 86 | version = "1.4.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 89 | 90 | [[package]] 91 | name = "backtrace" 92 | version = "0.3.74" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 95 | dependencies = [ 96 | "addr2line", 97 | "cfg-if", 98 | "libc", 99 | "miniz_oxide", 100 | "object", 101 | "rustc-demangle", 102 | "windows-targets", 103 | ] 104 | 105 | [[package]] 106 | name = "base64" 107 | version = "0.22.1" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 110 | 111 | [[package]] 112 | name = "bindgen" 113 | version = "0.70.1" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" 116 | dependencies = [ 117 | "bitflags", 118 | "cexpr", 119 | "clang-sys", 120 | "itertools", 121 | "proc-macro2", 122 | "quote", 123 | "regex", 124 | "rustc-hash", 125 | "shlex", 126 | "syn", 127 | ] 128 | 129 | [[package]] 130 | name = "bitflags" 131 | version = "2.9.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 134 | 135 | [[package]] 136 | name = "boring-sys2" 137 | version = "4.15.11" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "318dea1d26e76320786f325d0bc39b27a73a245c64dccf9c04d89f5af2256db0" 140 | dependencies = [ 141 | "autocfg", 142 | "bindgen", 143 | "cmake", 144 | "fs_extra", 145 | "fslock", 146 | ] 147 | 148 | [[package]] 149 | name = "boring2" 150 | version = "4.15.11" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "eaab3849155b901770a35391d4cb6e0aa050475f70258aab6b92e356922d4d5a" 153 | dependencies = [ 154 | "bitflags", 155 | "boring-sys2", 156 | "foreign-types", 157 | "libc", 158 | "openssl-macros", 159 | ] 160 | 161 | [[package]] 162 | name = "brotli" 163 | version = "7.0.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" 166 | dependencies = [ 167 | "alloc-no-stdlib", 168 | "alloc-stdlib", 169 | "brotli-decompressor", 170 | ] 171 | 172 | [[package]] 173 | name = "brotli-decompressor" 174 | version = "4.0.2" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" 177 | dependencies = [ 178 | "alloc-no-stdlib", 179 | "alloc-stdlib", 180 | ] 181 | 182 | [[package]] 183 | name = "bytes" 184 | version = "1.10.1" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 187 | 188 | [[package]] 189 | name = "cc" 190 | version = "1.2.19" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" 193 | dependencies = [ 194 | "jobserver", 195 | "libc", 196 | "shlex", 197 | ] 198 | 199 | [[package]] 200 | name = "cexpr" 201 | version = "0.6.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 204 | dependencies = [ 205 | "nom", 206 | ] 207 | 208 | [[package]] 209 | name = "cfg-if" 210 | version = "1.0.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 213 | 214 | [[package]] 215 | name = "clang-sys" 216 | version = "1.8.1" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 219 | dependencies = [ 220 | "glob", 221 | "libc", 222 | "libloading", 223 | ] 224 | 225 | [[package]] 226 | name = "cmake" 227 | version = "0.1.54" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" 230 | dependencies = [ 231 | "cc", 232 | ] 233 | 234 | [[package]] 235 | name = "cookie" 236 | version = "0.18.1" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" 239 | dependencies = [ 240 | "percent-encoding", 241 | "time", 242 | "version_check", 243 | ] 244 | 245 | [[package]] 246 | name = "cookie_store" 247 | version = "0.21.1" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" 250 | dependencies = [ 251 | "cookie", 252 | "document-features", 253 | "idna", 254 | "log", 255 | "publicsuffix", 256 | "serde", 257 | "serde_derive", 258 | "serde_json", 259 | "time", 260 | "url", 261 | ] 262 | 263 | [[package]] 264 | name = "core-foundation" 265 | version = "0.9.4" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 268 | dependencies = [ 269 | "core-foundation-sys", 270 | "libc", 271 | ] 272 | 273 | [[package]] 274 | name = "core-foundation-sys" 275 | version = "0.8.7" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 278 | 279 | [[package]] 280 | name = "crc32fast" 281 | version = "1.4.2" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 284 | dependencies = [ 285 | "cfg-if", 286 | ] 287 | 288 | [[package]] 289 | name = "deranged" 290 | version = "0.4.0" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 293 | dependencies = [ 294 | "powerfmt", 295 | ] 296 | 297 | [[package]] 298 | name = "displaydoc" 299 | version = "0.2.5" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 302 | dependencies = [ 303 | "proc-macro2", 304 | "quote", 305 | "syn", 306 | ] 307 | 308 | [[package]] 309 | name = "document-features" 310 | version = "0.2.11" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 313 | dependencies = [ 314 | "litrs", 315 | ] 316 | 317 | [[package]] 318 | name = "either" 319 | version = "1.15.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 322 | 323 | [[package]] 324 | name = "encoding_rs" 325 | version = "0.8.35" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 328 | dependencies = [ 329 | "cfg-if", 330 | ] 331 | 332 | [[package]] 333 | name = "equivalent" 334 | version = "1.0.2" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 337 | 338 | [[package]] 339 | name = "flate2" 340 | version = "1.1.1" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 343 | dependencies = [ 344 | "crc32fast", 345 | "miniz_oxide", 346 | ] 347 | 348 | [[package]] 349 | name = "fnv" 350 | version = "1.0.7" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 353 | 354 | [[package]] 355 | name = "foldhash" 356 | version = "0.1.5" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 359 | 360 | [[package]] 361 | name = "foreign-types" 362 | version = "0.5.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" 365 | dependencies = [ 366 | "foreign-types-macros", 367 | "foreign-types-shared", 368 | ] 369 | 370 | [[package]] 371 | name = "foreign-types-macros" 372 | version = "0.2.3" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" 375 | dependencies = [ 376 | "proc-macro2", 377 | "quote", 378 | "syn", 379 | ] 380 | 381 | [[package]] 382 | name = "foreign-types-shared" 383 | version = "0.3.1" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" 386 | 387 | [[package]] 388 | name = "form_urlencoded" 389 | version = "1.2.1" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 392 | dependencies = [ 393 | "percent-encoding", 394 | ] 395 | 396 | [[package]] 397 | name = "fs_extra" 398 | version = "1.3.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 401 | 402 | [[package]] 403 | name = "fslock" 404 | version = "0.2.1" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" 407 | dependencies = [ 408 | "libc", 409 | "winapi", 410 | ] 411 | 412 | [[package]] 413 | name = "futf" 414 | version = "0.1.5" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" 417 | dependencies = [ 418 | "mac", 419 | "new_debug_unreachable", 420 | ] 421 | 422 | [[package]] 423 | name = "futures-channel" 424 | version = "0.3.31" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 427 | dependencies = [ 428 | "futures-core", 429 | ] 430 | 431 | [[package]] 432 | name = "futures-core" 433 | version = "0.3.31" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 436 | 437 | [[package]] 438 | name = "futures-sink" 439 | version = "0.3.31" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 442 | 443 | [[package]] 444 | name = "futures-task" 445 | version = "0.3.31" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 448 | 449 | [[package]] 450 | name = "futures-util" 451 | version = "0.3.31" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 454 | dependencies = [ 455 | "futures-core", 456 | "futures-task", 457 | "pin-project-lite", 458 | "pin-utils", 459 | ] 460 | 461 | [[package]] 462 | name = "getrandom" 463 | version = "0.3.2" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 466 | dependencies = [ 467 | "cfg-if", 468 | "libc", 469 | "r-efi", 470 | "wasi 0.14.2+wasi-0.2.4", 471 | ] 472 | 473 | [[package]] 474 | name = "gimli" 475 | version = "0.31.1" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 478 | 479 | [[package]] 480 | name = "glob" 481 | version = "0.3.2" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 484 | 485 | [[package]] 486 | name = "hashbrown" 487 | version = "0.15.2" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 490 | 491 | [[package]] 492 | name = "heck" 493 | version = "0.5.0" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 496 | 497 | [[package]] 498 | name = "html2text" 499 | version = "0.14.3" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "393aaeda74fd1ee299520131edd11dbbeda69dd0a88965cc4a71945b78439fe9" 502 | dependencies = [ 503 | "html5ever", 504 | "tendril", 505 | "thiserror 2.0.12", 506 | "unicode-width", 507 | ] 508 | 509 | [[package]] 510 | name = "html5ever" 511 | version = "0.31.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "953cbbe631aae7fc0a112702ad5d3aaf09da38beaf45ea84610d6e1c358f569c" 514 | dependencies = [ 515 | "log", 516 | "mac", 517 | "markup5ever", 518 | "match_token", 519 | ] 520 | 521 | [[package]] 522 | name = "http" 523 | version = "1.3.1" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 526 | dependencies = [ 527 | "bytes", 528 | "fnv", 529 | "itoa", 530 | ] 531 | 532 | [[package]] 533 | name = "http-body" 534 | version = "1.0.1" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 537 | dependencies = [ 538 | "bytes", 539 | "http", 540 | ] 541 | 542 | [[package]] 543 | name = "http-body-util" 544 | version = "0.1.3" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 547 | dependencies = [ 548 | "bytes", 549 | "futures-core", 550 | "http", 551 | "http-body", 552 | "pin-project-lite", 553 | ] 554 | 555 | [[package]] 556 | name = "http2" 557 | version = "0.4.21" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "a6e23815f8ec982e1452e1d0fda921ec20a9187fb610ad003c90cc5abd65b2c4" 560 | dependencies = [ 561 | "atomic-waker", 562 | "bytes", 563 | "fnv", 564 | "futures-core", 565 | "futures-sink", 566 | "http", 567 | "indexmap", 568 | "slab", 569 | "tokio", 570 | "tokio-util", 571 | ] 572 | 573 | [[package]] 574 | name = "httparse" 575 | version = "1.10.1" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 578 | 579 | [[package]] 580 | name = "hyper2" 581 | version = "1.5.5" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "d1a1bb25e0cbdbe7b21f8ef6c3c4785f147d79e7ded438b5ee2b70e380183294" 584 | dependencies = [ 585 | "bytes", 586 | "futures-channel", 587 | "futures-util", 588 | "http", 589 | "http-body", 590 | "http2", 591 | "httparse", 592 | "itoa", 593 | "pin-project-lite", 594 | "smallvec", 595 | "tokio", 596 | "want", 597 | ] 598 | 599 | [[package]] 600 | name = "icu_collections" 601 | version = "1.5.0" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 604 | dependencies = [ 605 | "displaydoc", 606 | "yoke", 607 | "zerofrom", 608 | "zerovec", 609 | ] 610 | 611 | [[package]] 612 | name = "icu_locid" 613 | version = "1.5.0" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 616 | dependencies = [ 617 | "displaydoc", 618 | "litemap", 619 | "tinystr", 620 | "writeable", 621 | "zerovec", 622 | ] 623 | 624 | [[package]] 625 | name = "icu_locid_transform" 626 | version = "1.5.0" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 629 | dependencies = [ 630 | "displaydoc", 631 | "icu_locid", 632 | "icu_locid_transform_data", 633 | "icu_provider", 634 | "tinystr", 635 | "zerovec", 636 | ] 637 | 638 | [[package]] 639 | name = "icu_locid_transform_data" 640 | version = "1.5.1" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 643 | 644 | [[package]] 645 | name = "icu_normalizer" 646 | version = "1.5.0" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 649 | dependencies = [ 650 | "displaydoc", 651 | "icu_collections", 652 | "icu_normalizer_data", 653 | "icu_properties", 654 | "icu_provider", 655 | "smallvec", 656 | "utf16_iter", 657 | "utf8_iter", 658 | "write16", 659 | "zerovec", 660 | ] 661 | 662 | [[package]] 663 | name = "icu_normalizer_data" 664 | version = "1.5.1" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 667 | 668 | [[package]] 669 | name = "icu_properties" 670 | version = "1.5.1" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 673 | dependencies = [ 674 | "displaydoc", 675 | "icu_collections", 676 | "icu_locid_transform", 677 | "icu_properties_data", 678 | "icu_provider", 679 | "tinystr", 680 | "zerovec", 681 | ] 682 | 683 | [[package]] 684 | name = "icu_properties_data" 685 | version = "1.5.1" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 688 | 689 | [[package]] 690 | name = "icu_provider" 691 | version = "1.5.0" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 694 | dependencies = [ 695 | "displaydoc", 696 | "icu_locid", 697 | "icu_provider_macros", 698 | "stable_deref_trait", 699 | "tinystr", 700 | "writeable", 701 | "yoke", 702 | "zerofrom", 703 | "zerovec", 704 | ] 705 | 706 | [[package]] 707 | name = "icu_provider_macros" 708 | version = "1.5.0" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 711 | dependencies = [ 712 | "proc-macro2", 713 | "quote", 714 | "syn", 715 | ] 716 | 717 | [[package]] 718 | name = "idna" 719 | version = "1.0.3" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 722 | dependencies = [ 723 | "idna_adapter", 724 | "smallvec", 725 | "utf8_iter", 726 | ] 727 | 728 | [[package]] 729 | name = "idna_adapter" 730 | version = "1.2.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 733 | dependencies = [ 734 | "icu_normalizer", 735 | "icu_properties", 736 | ] 737 | 738 | [[package]] 739 | name = "indexmap" 740 | version = "2.9.0" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 743 | dependencies = [ 744 | "equivalent", 745 | "hashbrown", 746 | "serde", 747 | ] 748 | 749 | [[package]] 750 | name = "indoc" 751 | version = "2.0.6" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 754 | 755 | [[package]] 756 | name = "inventory" 757 | version = "0.3.20" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" 760 | dependencies = [ 761 | "rustversion", 762 | ] 763 | 764 | [[package]] 765 | name = "ipnet" 766 | version = "2.11.0" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 769 | 770 | [[package]] 771 | name = "itertools" 772 | version = "0.13.0" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 775 | dependencies = [ 776 | "either", 777 | ] 778 | 779 | [[package]] 780 | name = "itoa" 781 | version = "1.0.15" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 784 | 785 | [[package]] 786 | name = "jobserver" 787 | version = "0.1.33" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 790 | dependencies = [ 791 | "getrandom", 792 | "libc", 793 | ] 794 | 795 | [[package]] 796 | name = "libc" 797 | version = "0.2.172" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 800 | 801 | [[package]] 802 | name = "libloading" 803 | version = "0.8.6" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" 806 | dependencies = [ 807 | "cfg-if", 808 | "windows-targets", 809 | ] 810 | 811 | [[package]] 812 | name = "linked-hash-map" 813 | version = "0.5.6" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 816 | 817 | [[package]] 818 | name = "linked_hash_set" 819 | version = "0.1.5" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "bae85b5be22d9843c80e5fc80e9b64c8a3b1f98f867c709956eca3efff4e92e2" 822 | dependencies = [ 823 | "linked-hash-map", 824 | ] 825 | 826 | [[package]] 827 | name = "litemap" 828 | version = "0.7.5" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 831 | 832 | [[package]] 833 | name = "litrs" 834 | version = "0.4.1" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 837 | 838 | [[package]] 839 | name = "lock_api" 840 | version = "0.4.12" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 843 | dependencies = [ 844 | "autocfg", 845 | "scopeguard", 846 | ] 847 | 848 | [[package]] 849 | name = "log" 850 | version = "0.4.27" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 853 | 854 | [[package]] 855 | name = "lru" 856 | version = "0.13.0" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" 859 | 860 | [[package]] 861 | name = "mac" 862 | version = "0.1.1" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 865 | 866 | [[package]] 867 | name = "markup5ever" 868 | version = "0.16.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "0ba2225413ed418d540a2c8247d794f4b0527a021da36f69c05344d716dc44c1" 871 | dependencies = [ 872 | "log", 873 | "phf", 874 | "phf_codegen", 875 | "string_cache", 876 | "string_cache_codegen", 877 | "tendril", 878 | ] 879 | 880 | [[package]] 881 | name = "match_token" 882 | version = "0.1.0" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" 885 | dependencies = [ 886 | "proc-macro2", 887 | "quote", 888 | "syn", 889 | ] 890 | 891 | [[package]] 892 | name = "memchr" 893 | version = "2.7.4" 894 | source = "registry+https://github.com/rust-lang/crates.io-index" 895 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 896 | 897 | [[package]] 898 | name = "memoffset" 899 | version = "0.9.1" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" 902 | dependencies = [ 903 | "autocfg", 904 | ] 905 | 906 | [[package]] 907 | name = "mime" 908 | version = "0.3.17" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 911 | 912 | [[package]] 913 | name = "mime_guess" 914 | version = "2.0.5" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 917 | dependencies = [ 918 | "mime", 919 | "unicase", 920 | ] 921 | 922 | [[package]] 923 | name = "minimal-lexical" 924 | version = "0.2.1" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 927 | 928 | [[package]] 929 | name = "miniz_oxide" 930 | version = "0.8.8" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 933 | dependencies = [ 934 | "adler2", 935 | ] 936 | 937 | [[package]] 938 | name = "mio" 939 | version = "1.0.3" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 942 | dependencies = [ 943 | "libc", 944 | "wasi 0.11.0+wasi-snapshot-preview1", 945 | "windows-sys", 946 | ] 947 | 948 | [[package]] 949 | name = "new_debug_unreachable" 950 | version = "1.0.6" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 953 | 954 | [[package]] 955 | name = "nom" 956 | version = "7.1.3" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 959 | dependencies = [ 960 | "memchr", 961 | "minimal-lexical", 962 | ] 963 | 964 | [[package]] 965 | name = "num-conv" 966 | version = "0.1.0" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 969 | 970 | [[package]] 971 | name = "object" 972 | version = "0.36.7" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 975 | dependencies = [ 976 | "memchr", 977 | ] 978 | 979 | [[package]] 980 | name = "once_cell" 981 | version = "1.21.3" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 984 | 985 | [[package]] 986 | name = "openssl-macros" 987 | version = "0.1.1" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 990 | dependencies = [ 991 | "proc-macro2", 992 | "quote", 993 | "syn", 994 | ] 995 | 996 | [[package]] 997 | name = "parking_lot" 998 | version = "0.12.3" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1001 | dependencies = [ 1002 | "lock_api", 1003 | "parking_lot_core", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "parking_lot_core" 1008 | version = "0.9.10" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1011 | dependencies = [ 1012 | "cfg-if", 1013 | "libc", 1014 | "redox_syscall", 1015 | "smallvec", 1016 | "windows-targets", 1017 | ] 1018 | 1019 | [[package]] 1020 | name = "percent-encoding" 1021 | version = "2.3.1" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1024 | 1025 | [[package]] 1026 | name = "phf" 1027 | version = "0.11.3" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 1030 | dependencies = [ 1031 | "phf_shared", 1032 | ] 1033 | 1034 | [[package]] 1035 | name = "phf_codegen" 1036 | version = "0.11.3" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 1039 | dependencies = [ 1040 | "phf_generator", 1041 | "phf_shared", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "phf_generator" 1046 | version = "0.11.3" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 1049 | dependencies = [ 1050 | "phf_shared", 1051 | "rand 0.8.5", 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "phf_shared" 1056 | version = "0.11.3" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 1059 | dependencies = [ 1060 | "siphasher", 1061 | ] 1062 | 1063 | [[package]] 1064 | name = "pin-project-lite" 1065 | version = "0.2.16" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1068 | 1069 | [[package]] 1070 | name = "pin-utils" 1071 | version = "0.1.0" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1074 | 1075 | [[package]] 1076 | name = "pkg-config" 1077 | version = "0.3.32" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1080 | 1081 | [[package]] 1082 | name = "portable-atomic" 1083 | version = "1.11.0" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 1086 | 1087 | [[package]] 1088 | name = "powerfmt" 1089 | version = "0.2.0" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1092 | 1093 | [[package]] 1094 | name = "ppv-lite86" 1095 | version = "0.2.21" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1098 | dependencies = [ 1099 | "zerocopy", 1100 | ] 1101 | 1102 | [[package]] 1103 | name = "precomputed-hash" 1104 | version = "0.1.1" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 1107 | 1108 | [[package]] 1109 | name = "primp" 1110 | version = "0.15.0" 1111 | dependencies = [ 1112 | "anyhow", 1113 | "encoding_rs", 1114 | "foldhash", 1115 | "html2text", 1116 | "http", 1117 | "http-body-util", 1118 | "indexmap", 1119 | "mime", 1120 | "pyo3", 1121 | "pyo3-log", 1122 | "pythonize", 1123 | "rand 0.9.0", 1124 | "rquest", 1125 | "serde_json", 1126 | "tokio", 1127 | "tokio-util", 1128 | "tracing", 1129 | "webpki-root-certs", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "proc-macro2" 1134 | version = "1.0.95" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 1137 | dependencies = [ 1138 | "unicode-ident", 1139 | ] 1140 | 1141 | [[package]] 1142 | name = "psl-types" 1143 | version = "2.0.11" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" 1146 | 1147 | [[package]] 1148 | name = "publicsuffix" 1149 | version = "2.3.0" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" 1152 | dependencies = [ 1153 | "idna", 1154 | "psl-types", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "pyo3" 1159 | version = "0.24.1" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229" 1162 | dependencies = [ 1163 | "anyhow", 1164 | "cfg-if", 1165 | "indexmap", 1166 | "indoc", 1167 | "inventory", 1168 | "libc", 1169 | "memoffset", 1170 | "once_cell", 1171 | "portable-atomic", 1172 | "pyo3-build-config", 1173 | "pyo3-ffi", 1174 | "pyo3-macros", 1175 | "unindent", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "pyo3-build-config" 1180 | version = "0.24.1" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1" 1183 | dependencies = [ 1184 | "once_cell", 1185 | "target-lexicon", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "pyo3-ffi" 1190 | version = "0.24.1" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc" 1193 | dependencies = [ 1194 | "libc", 1195 | "pyo3-build-config", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "pyo3-log" 1200 | version = "0.12.3" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "7079e412e909af5d6be7c04a7f29f6a2837a080410e1c529c9dee2c367383db4" 1203 | dependencies = [ 1204 | "arc-swap", 1205 | "log", 1206 | "pyo3", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "pyo3-macros" 1211 | version = "0.24.1" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44" 1214 | dependencies = [ 1215 | "proc-macro2", 1216 | "pyo3-macros-backend", 1217 | "quote", 1218 | "syn", 1219 | ] 1220 | 1221 | [[package]] 1222 | name = "pyo3-macros-backend" 1223 | version = "0.24.1" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855" 1226 | dependencies = [ 1227 | "heck", 1228 | "proc-macro2", 1229 | "pyo3-build-config", 1230 | "quote", 1231 | "syn", 1232 | ] 1233 | 1234 | [[package]] 1235 | name = "pythonize" 1236 | version = "0.24.0" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "d5bcac0d0b71821f0d69e42654f1e15e5c94b85196446c4de9588951a2117e7b" 1239 | dependencies = [ 1240 | "pyo3", 1241 | "serde", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "quote" 1246 | version = "1.0.40" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1249 | dependencies = [ 1250 | "proc-macro2", 1251 | ] 1252 | 1253 | [[package]] 1254 | name = "r-efi" 1255 | version = "5.2.0" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1258 | 1259 | [[package]] 1260 | name = "rand" 1261 | version = "0.8.5" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1264 | dependencies = [ 1265 | "rand_core 0.6.4", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "rand" 1270 | version = "0.9.0" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 1273 | dependencies = [ 1274 | "rand_chacha", 1275 | "rand_core 0.9.3", 1276 | "zerocopy", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "rand_chacha" 1281 | version = "0.9.0" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1284 | dependencies = [ 1285 | "ppv-lite86", 1286 | "rand_core 0.9.3", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "rand_core" 1291 | version = "0.6.4" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1294 | 1295 | [[package]] 1296 | name = "rand_core" 1297 | version = "0.9.3" 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" 1299 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1300 | dependencies = [ 1301 | "getrandom", 1302 | ] 1303 | 1304 | [[package]] 1305 | name = "redox_syscall" 1306 | version = "0.5.11" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" 1309 | dependencies = [ 1310 | "bitflags", 1311 | ] 1312 | 1313 | [[package]] 1314 | name = "regex" 1315 | version = "1.11.1" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1318 | dependencies = [ 1319 | "aho-corasick", 1320 | "memchr", 1321 | "regex-automata", 1322 | "regex-syntax", 1323 | ] 1324 | 1325 | [[package]] 1326 | name = "regex-automata" 1327 | version = "0.4.9" 1328 | source = "registry+https://github.com/rust-lang/crates.io-index" 1329 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1330 | dependencies = [ 1331 | "aho-corasick", 1332 | "memchr", 1333 | "regex-syntax", 1334 | ] 1335 | 1336 | [[package]] 1337 | name = "regex-syntax" 1338 | version = "0.8.5" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1341 | 1342 | [[package]] 1343 | name = "rquest" 1344 | version = "2.2.1" 1345 | source = "registry+https://github.com/rust-lang/crates.io-index" 1346 | checksum = "b5037287a38246515a36794ee0e475ea0bd4f3873c06229680287a52dc428ec9" 1347 | dependencies = [ 1348 | "antidote", 1349 | "async-compression", 1350 | "base64", 1351 | "boring-sys2", 1352 | "boring2", 1353 | "brotli", 1354 | "bytes", 1355 | "cookie", 1356 | "cookie_store", 1357 | "encoding_rs", 1358 | "flate2", 1359 | "foreign-types", 1360 | "futures-util", 1361 | "http", 1362 | "http-body", 1363 | "http-body-util", 1364 | "hyper2", 1365 | "ipnet", 1366 | "libc", 1367 | "linked_hash_set", 1368 | "log", 1369 | "lru", 1370 | "mime", 1371 | "mime_guess", 1372 | "percent-encoding", 1373 | "pin-project-lite", 1374 | "serde", 1375 | "serde_json", 1376 | "serde_urlencoded", 1377 | "socket2", 1378 | "sync_wrapper", 1379 | "system-configuration", 1380 | "tokio", 1381 | "tokio-boring2", 1382 | "tokio-socks", 1383 | "tokio-util", 1384 | "tower", 1385 | "tower-service", 1386 | "typed-builder", 1387 | "url", 1388 | "webpki-root-certs", 1389 | "windows-registry", 1390 | "zstd", 1391 | ] 1392 | 1393 | [[package]] 1394 | name = "rustc-demangle" 1395 | version = "0.1.24" 1396 | source = "registry+https://github.com/rust-lang/crates.io-index" 1397 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1398 | 1399 | [[package]] 1400 | name = "rustc-hash" 1401 | version = "1.1.0" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1404 | 1405 | [[package]] 1406 | name = "rustls-pki-types" 1407 | version = "1.11.0" 1408 | source = "registry+https://github.com/rust-lang/crates.io-index" 1409 | checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 1410 | 1411 | [[package]] 1412 | name = "rustversion" 1413 | version = "1.0.20" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1416 | 1417 | [[package]] 1418 | name = "ryu" 1419 | version = "1.0.20" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1422 | 1423 | [[package]] 1424 | name = "scopeguard" 1425 | version = "1.2.0" 1426 | source = "registry+https://github.com/rust-lang/crates.io-index" 1427 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1428 | 1429 | [[package]] 1430 | name = "serde" 1431 | version = "1.0.219" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1434 | dependencies = [ 1435 | "serde_derive", 1436 | ] 1437 | 1438 | [[package]] 1439 | name = "serde_derive" 1440 | version = "1.0.219" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1443 | dependencies = [ 1444 | "proc-macro2", 1445 | "quote", 1446 | "syn", 1447 | ] 1448 | 1449 | [[package]] 1450 | name = "serde_json" 1451 | version = "1.0.140" 1452 | source = "registry+https://github.com/rust-lang/crates.io-index" 1453 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1454 | dependencies = [ 1455 | "itoa", 1456 | "memchr", 1457 | "ryu", 1458 | "serde", 1459 | ] 1460 | 1461 | [[package]] 1462 | name = "serde_urlencoded" 1463 | version = "0.7.1" 1464 | source = "registry+https://github.com/rust-lang/crates.io-index" 1465 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1466 | dependencies = [ 1467 | "form_urlencoded", 1468 | "itoa", 1469 | "ryu", 1470 | "serde", 1471 | ] 1472 | 1473 | [[package]] 1474 | name = "shlex" 1475 | version = "1.3.0" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1478 | 1479 | [[package]] 1480 | name = "signal-hook-registry" 1481 | version = "1.4.2" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1484 | dependencies = [ 1485 | "libc", 1486 | ] 1487 | 1488 | [[package]] 1489 | name = "siphasher" 1490 | version = "1.0.1" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 1493 | 1494 | [[package]] 1495 | name = "slab" 1496 | version = "0.4.9" 1497 | source = "registry+https://github.com/rust-lang/crates.io-index" 1498 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1499 | dependencies = [ 1500 | "autocfg", 1501 | ] 1502 | 1503 | [[package]] 1504 | name = "smallvec" 1505 | version = "1.15.0" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1508 | 1509 | [[package]] 1510 | name = "socket2" 1511 | version = "0.5.9" 1512 | source = "registry+https://github.com/rust-lang/crates.io-index" 1513 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1514 | dependencies = [ 1515 | "libc", 1516 | "windows-sys", 1517 | ] 1518 | 1519 | [[package]] 1520 | name = "stable_deref_trait" 1521 | version = "1.2.0" 1522 | source = "registry+https://github.com/rust-lang/crates.io-index" 1523 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1524 | 1525 | [[package]] 1526 | name = "string_cache" 1527 | version = "0.8.9" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" 1530 | dependencies = [ 1531 | "new_debug_unreachable", 1532 | "parking_lot", 1533 | "phf_shared", 1534 | "precomputed-hash", 1535 | "serde", 1536 | ] 1537 | 1538 | [[package]] 1539 | name = "string_cache_codegen" 1540 | version = "0.5.4" 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" 1542 | checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" 1543 | dependencies = [ 1544 | "phf_generator", 1545 | "phf_shared", 1546 | "proc-macro2", 1547 | "quote", 1548 | ] 1549 | 1550 | [[package]] 1551 | name = "syn" 1552 | version = "2.0.100" 1553 | source = "registry+https://github.com/rust-lang/crates.io-index" 1554 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 1555 | dependencies = [ 1556 | "proc-macro2", 1557 | "quote", 1558 | "unicode-ident", 1559 | ] 1560 | 1561 | [[package]] 1562 | name = "sync_wrapper" 1563 | version = "1.0.2" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1566 | dependencies = [ 1567 | "futures-core", 1568 | ] 1569 | 1570 | [[package]] 1571 | name = "synstructure" 1572 | version = "0.13.1" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1575 | dependencies = [ 1576 | "proc-macro2", 1577 | "quote", 1578 | "syn", 1579 | ] 1580 | 1581 | [[package]] 1582 | name = "system-configuration" 1583 | version = "0.6.1" 1584 | source = "registry+https://github.com/rust-lang/crates.io-index" 1585 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1586 | dependencies = [ 1587 | "bitflags", 1588 | "core-foundation", 1589 | "system-configuration-sys", 1590 | ] 1591 | 1592 | [[package]] 1593 | name = "system-configuration-sys" 1594 | version = "0.6.0" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1597 | dependencies = [ 1598 | "core-foundation-sys", 1599 | "libc", 1600 | ] 1601 | 1602 | [[package]] 1603 | name = "target-lexicon" 1604 | version = "0.13.2" 1605 | source = "registry+https://github.com/rust-lang/crates.io-index" 1606 | checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" 1607 | 1608 | [[package]] 1609 | name = "tendril" 1610 | version = "0.4.3" 1611 | source = "registry+https://github.com/rust-lang/crates.io-index" 1612 | checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" 1613 | dependencies = [ 1614 | "futf", 1615 | "mac", 1616 | "utf-8", 1617 | ] 1618 | 1619 | [[package]] 1620 | name = "thiserror" 1621 | version = "1.0.69" 1622 | source = "registry+https://github.com/rust-lang/crates.io-index" 1623 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1624 | dependencies = [ 1625 | "thiserror-impl 1.0.69", 1626 | ] 1627 | 1628 | [[package]] 1629 | name = "thiserror" 1630 | version = "2.0.12" 1631 | source = "registry+https://github.com/rust-lang/crates.io-index" 1632 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1633 | dependencies = [ 1634 | "thiserror-impl 2.0.12", 1635 | ] 1636 | 1637 | [[package]] 1638 | name = "thiserror-impl" 1639 | version = "1.0.69" 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" 1641 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1642 | dependencies = [ 1643 | "proc-macro2", 1644 | "quote", 1645 | "syn", 1646 | ] 1647 | 1648 | [[package]] 1649 | name = "thiserror-impl" 1650 | version = "2.0.12" 1651 | source = "registry+https://github.com/rust-lang/crates.io-index" 1652 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1653 | dependencies = [ 1654 | "proc-macro2", 1655 | "quote", 1656 | "syn", 1657 | ] 1658 | 1659 | [[package]] 1660 | name = "time" 1661 | version = "0.3.41" 1662 | source = "registry+https://github.com/rust-lang/crates.io-index" 1663 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 1664 | dependencies = [ 1665 | "deranged", 1666 | "itoa", 1667 | "num-conv", 1668 | "powerfmt", 1669 | "serde", 1670 | "time-core", 1671 | "time-macros", 1672 | ] 1673 | 1674 | [[package]] 1675 | name = "time-core" 1676 | version = "0.1.4" 1677 | source = "registry+https://github.com/rust-lang/crates.io-index" 1678 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 1679 | 1680 | [[package]] 1681 | name = "time-macros" 1682 | version = "0.2.22" 1683 | source = "registry+https://github.com/rust-lang/crates.io-index" 1684 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 1685 | dependencies = [ 1686 | "num-conv", 1687 | "time-core", 1688 | ] 1689 | 1690 | [[package]] 1691 | name = "tinystr" 1692 | version = "0.7.6" 1693 | source = "registry+https://github.com/rust-lang/crates.io-index" 1694 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1695 | dependencies = [ 1696 | "displaydoc", 1697 | "zerovec", 1698 | ] 1699 | 1700 | [[package]] 1701 | name = "tokio" 1702 | version = "1.44.2" 1703 | source = "registry+https://github.com/rust-lang/crates.io-index" 1704 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 1705 | dependencies = [ 1706 | "backtrace", 1707 | "bytes", 1708 | "libc", 1709 | "mio", 1710 | "parking_lot", 1711 | "pin-project-lite", 1712 | "signal-hook-registry", 1713 | "socket2", 1714 | "tokio-macros", 1715 | "windows-sys", 1716 | ] 1717 | 1718 | [[package]] 1719 | name = "tokio-boring2" 1720 | version = "4.15.11" 1721 | source = "registry+https://github.com/rust-lang/crates.io-index" 1722 | checksum = "17321b261a835fc4b7211ca05011d7ffbd4e355bc29fb8098d654798f54def90" 1723 | dependencies = [ 1724 | "boring-sys2", 1725 | "boring2", 1726 | "tokio", 1727 | ] 1728 | 1729 | [[package]] 1730 | name = "tokio-macros" 1731 | version = "2.5.0" 1732 | source = "registry+https://github.com/rust-lang/crates.io-index" 1733 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1734 | dependencies = [ 1735 | "proc-macro2", 1736 | "quote", 1737 | "syn", 1738 | ] 1739 | 1740 | [[package]] 1741 | name = "tokio-socks" 1742 | version = "0.5.2" 1743 | source = "registry+https://github.com/rust-lang/crates.io-index" 1744 | checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" 1745 | dependencies = [ 1746 | "either", 1747 | "futures-util", 1748 | "thiserror 1.0.69", 1749 | "tokio", 1750 | ] 1751 | 1752 | [[package]] 1753 | name = "tokio-util" 1754 | version = "0.7.14" 1755 | source = "registry+https://github.com/rust-lang/crates.io-index" 1756 | checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" 1757 | dependencies = [ 1758 | "bytes", 1759 | "futures-core", 1760 | "futures-sink", 1761 | "pin-project-lite", 1762 | "tokio", 1763 | ] 1764 | 1765 | [[package]] 1766 | name = "tower" 1767 | version = "0.5.2" 1768 | source = "registry+https://github.com/rust-lang/crates.io-index" 1769 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1770 | dependencies = [ 1771 | "futures-core", 1772 | "futures-util", 1773 | "pin-project-lite", 1774 | "sync_wrapper", 1775 | "tokio", 1776 | "tower-layer", 1777 | "tower-service", 1778 | ] 1779 | 1780 | [[package]] 1781 | name = "tower-layer" 1782 | version = "0.3.3" 1783 | source = "registry+https://github.com/rust-lang/crates.io-index" 1784 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1785 | 1786 | [[package]] 1787 | name = "tower-service" 1788 | version = "0.3.3" 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" 1790 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1791 | 1792 | [[package]] 1793 | name = "tracing" 1794 | version = "0.1.41" 1795 | source = "registry+https://github.com/rust-lang/crates.io-index" 1796 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1797 | dependencies = [ 1798 | "log", 1799 | "pin-project-lite", 1800 | "tracing-attributes", 1801 | "tracing-core", 1802 | ] 1803 | 1804 | [[package]] 1805 | name = "tracing-attributes" 1806 | version = "0.1.28" 1807 | source = "registry+https://github.com/rust-lang/crates.io-index" 1808 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 1809 | dependencies = [ 1810 | "proc-macro2", 1811 | "quote", 1812 | "syn", 1813 | ] 1814 | 1815 | [[package]] 1816 | name = "tracing-core" 1817 | version = "0.1.33" 1818 | source = "registry+https://github.com/rust-lang/crates.io-index" 1819 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1820 | dependencies = [ 1821 | "once_cell", 1822 | ] 1823 | 1824 | [[package]] 1825 | name = "try-lock" 1826 | version = "0.2.5" 1827 | source = "registry+https://github.com/rust-lang/crates.io-index" 1828 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1829 | 1830 | [[package]] 1831 | name = "typed-builder" 1832 | version = "0.20.1" 1833 | source = "registry+https://github.com/rust-lang/crates.io-index" 1834 | checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" 1835 | dependencies = [ 1836 | "typed-builder-macro", 1837 | ] 1838 | 1839 | [[package]] 1840 | name = "typed-builder-macro" 1841 | version = "0.20.1" 1842 | source = "registry+https://github.com/rust-lang/crates.io-index" 1843 | checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" 1844 | dependencies = [ 1845 | "proc-macro2", 1846 | "quote", 1847 | "syn", 1848 | ] 1849 | 1850 | [[package]] 1851 | name = "unicase" 1852 | version = "2.8.1" 1853 | source = "registry+https://github.com/rust-lang/crates.io-index" 1854 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 1855 | 1856 | [[package]] 1857 | name = "unicode-ident" 1858 | version = "1.0.18" 1859 | source = "registry+https://github.com/rust-lang/crates.io-index" 1860 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1861 | 1862 | [[package]] 1863 | name = "unicode-width" 1864 | version = "0.2.0" 1865 | source = "registry+https://github.com/rust-lang/crates.io-index" 1866 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1867 | 1868 | [[package]] 1869 | name = "unindent" 1870 | version = "0.2.4" 1871 | source = "registry+https://github.com/rust-lang/crates.io-index" 1872 | checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" 1873 | 1874 | [[package]] 1875 | name = "url" 1876 | version = "2.5.4" 1877 | source = "registry+https://github.com/rust-lang/crates.io-index" 1878 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1879 | dependencies = [ 1880 | "form_urlencoded", 1881 | "idna", 1882 | "percent-encoding", 1883 | ] 1884 | 1885 | [[package]] 1886 | name = "utf-8" 1887 | version = "0.7.6" 1888 | source = "registry+https://github.com/rust-lang/crates.io-index" 1889 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1890 | 1891 | [[package]] 1892 | name = "utf16_iter" 1893 | version = "1.0.5" 1894 | source = "registry+https://github.com/rust-lang/crates.io-index" 1895 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1896 | 1897 | [[package]] 1898 | name = "utf8_iter" 1899 | version = "1.0.4" 1900 | source = "registry+https://github.com/rust-lang/crates.io-index" 1901 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1902 | 1903 | [[package]] 1904 | name = "version_check" 1905 | version = "0.9.5" 1906 | source = "registry+https://github.com/rust-lang/crates.io-index" 1907 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1908 | 1909 | [[package]] 1910 | name = "want" 1911 | version = "0.3.1" 1912 | source = "registry+https://github.com/rust-lang/crates.io-index" 1913 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1914 | dependencies = [ 1915 | "try-lock", 1916 | ] 1917 | 1918 | [[package]] 1919 | name = "wasi" 1920 | version = "0.11.0+wasi-snapshot-preview1" 1921 | source = "registry+https://github.com/rust-lang/crates.io-index" 1922 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1923 | 1924 | [[package]] 1925 | name = "wasi" 1926 | version = "0.14.2+wasi-0.2.4" 1927 | source = "registry+https://github.com/rust-lang/crates.io-index" 1928 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1929 | dependencies = [ 1930 | "wit-bindgen-rt", 1931 | ] 1932 | 1933 | [[package]] 1934 | name = "webpki-root-certs" 1935 | version = "0.26.8" 1936 | source = "registry+https://github.com/rust-lang/crates.io-index" 1937 | checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" 1938 | dependencies = [ 1939 | "rustls-pki-types", 1940 | ] 1941 | 1942 | [[package]] 1943 | name = "winapi" 1944 | version = "0.3.9" 1945 | source = "registry+https://github.com/rust-lang/crates.io-index" 1946 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1947 | dependencies = [ 1948 | "winapi-i686-pc-windows-gnu", 1949 | "winapi-x86_64-pc-windows-gnu", 1950 | ] 1951 | 1952 | [[package]] 1953 | name = "winapi-i686-pc-windows-gnu" 1954 | version = "0.4.0" 1955 | source = "registry+https://github.com/rust-lang/crates.io-index" 1956 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1957 | 1958 | [[package]] 1959 | name = "winapi-x86_64-pc-windows-gnu" 1960 | version = "0.4.0" 1961 | source = "registry+https://github.com/rust-lang/crates.io-index" 1962 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1963 | 1964 | [[package]] 1965 | name = "windows-link" 1966 | version = "0.1.1" 1967 | source = "registry+https://github.com/rust-lang/crates.io-index" 1968 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 1969 | 1970 | [[package]] 1971 | name = "windows-registry" 1972 | version = "0.5.1" 1973 | source = "registry+https://github.com/rust-lang/crates.io-index" 1974 | checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e" 1975 | dependencies = [ 1976 | "windows-link", 1977 | "windows-result", 1978 | "windows-strings", 1979 | ] 1980 | 1981 | [[package]] 1982 | name = "windows-result" 1983 | version = "0.3.2" 1984 | source = "registry+https://github.com/rust-lang/crates.io-index" 1985 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 1986 | dependencies = [ 1987 | "windows-link", 1988 | ] 1989 | 1990 | [[package]] 1991 | name = "windows-strings" 1992 | version = "0.4.0" 1993 | source = "registry+https://github.com/rust-lang/crates.io-index" 1994 | checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 1995 | dependencies = [ 1996 | "windows-link", 1997 | ] 1998 | 1999 | [[package]] 2000 | name = "windows-sys" 2001 | version = "0.52.0" 2002 | source = "registry+https://github.com/rust-lang/crates.io-index" 2003 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2004 | dependencies = [ 2005 | "windows-targets", 2006 | ] 2007 | 2008 | [[package]] 2009 | name = "windows-targets" 2010 | version = "0.52.6" 2011 | source = "registry+https://github.com/rust-lang/crates.io-index" 2012 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2013 | dependencies = [ 2014 | "windows_aarch64_gnullvm", 2015 | "windows_aarch64_msvc", 2016 | "windows_i686_gnu", 2017 | "windows_i686_gnullvm", 2018 | "windows_i686_msvc", 2019 | "windows_x86_64_gnu", 2020 | "windows_x86_64_gnullvm", 2021 | "windows_x86_64_msvc", 2022 | ] 2023 | 2024 | [[package]] 2025 | name = "windows_aarch64_gnullvm" 2026 | version = "0.52.6" 2027 | source = "registry+https://github.com/rust-lang/crates.io-index" 2028 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2029 | 2030 | [[package]] 2031 | name = "windows_aarch64_msvc" 2032 | version = "0.52.6" 2033 | source = "registry+https://github.com/rust-lang/crates.io-index" 2034 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2035 | 2036 | [[package]] 2037 | name = "windows_i686_gnu" 2038 | version = "0.52.6" 2039 | source = "registry+https://github.com/rust-lang/crates.io-index" 2040 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2041 | 2042 | [[package]] 2043 | name = "windows_i686_gnullvm" 2044 | version = "0.52.6" 2045 | source = "registry+https://github.com/rust-lang/crates.io-index" 2046 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2047 | 2048 | [[package]] 2049 | name = "windows_i686_msvc" 2050 | version = "0.52.6" 2051 | source = "registry+https://github.com/rust-lang/crates.io-index" 2052 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2053 | 2054 | [[package]] 2055 | name = "windows_x86_64_gnu" 2056 | version = "0.52.6" 2057 | source = "registry+https://github.com/rust-lang/crates.io-index" 2058 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2059 | 2060 | [[package]] 2061 | name = "windows_x86_64_gnullvm" 2062 | version = "0.52.6" 2063 | source = "registry+https://github.com/rust-lang/crates.io-index" 2064 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2065 | 2066 | [[package]] 2067 | name = "windows_x86_64_msvc" 2068 | version = "0.52.6" 2069 | source = "registry+https://github.com/rust-lang/crates.io-index" 2070 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2071 | 2072 | [[package]] 2073 | name = "wit-bindgen-rt" 2074 | version = "0.39.0" 2075 | source = "registry+https://github.com/rust-lang/crates.io-index" 2076 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 2077 | dependencies = [ 2078 | "bitflags", 2079 | ] 2080 | 2081 | [[package]] 2082 | name = "write16" 2083 | version = "1.0.0" 2084 | source = "registry+https://github.com/rust-lang/crates.io-index" 2085 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 2086 | 2087 | [[package]] 2088 | name = "writeable" 2089 | version = "0.5.5" 2090 | source = "registry+https://github.com/rust-lang/crates.io-index" 2091 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 2092 | 2093 | [[package]] 2094 | name = "yoke" 2095 | version = "0.7.5" 2096 | source = "registry+https://github.com/rust-lang/crates.io-index" 2097 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 2098 | dependencies = [ 2099 | "serde", 2100 | "stable_deref_trait", 2101 | "yoke-derive", 2102 | "zerofrom", 2103 | ] 2104 | 2105 | [[package]] 2106 | name = "yoke-derive" 2107 | version = "0.7.5" 2108 | source = "registry+https://github.com/rust-lang/crates.io-index" 2109 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 2110 | dependencies = [ 2111 | "proc-macro2", 2112 | "quote", 2113 | "syn", 2114 | "synstructure", 2115 | ] 2116 | 2117 | [[package]] 2118 | name = "zerocopy" 2119 | version = "0.8.24" 2120 | source = "registry+https://github.com/rust-lang/crates.io-index" 2121 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 2122 | dependencies = [ 2123 | "zerocopy-derive", 2124 | ] 2125 | 2126 | [[package]] 2127 | name = "zerocopy-derive" 2128 | version = "0.8.24" 2129 | source = "registry+https://github.com/rust-lang/crates.io-index" 2130 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 2131 | dependencies = [ 2132 | "proc-macro2", 2133 | "quote", 2134 | "syn", 2135 | ] 2136 | 2137 | [[package]] 2138 | name = "zerofrom" 2139 | version = "0.1.6" 2140 | source = "registry+https://github.com/rust-lang/crates.io-index" 2141 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2142 | dependencies = [ 2143 | "zerofrom-derive", 2144 | ] 2145 | 2146 | [[package]] 2147 | name = "zerofrom-derive" 2148 | version = "0.1.6" 2149 | source = "registry+https://github.com/rust-lang/crates.io-index" 2150 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2151 | dependencies = [ 2152 | "proc-macro2", 2153 | "quote", 2154 | "syn", 2155 | "synstructure", 2156 | ] 2157 | 2158 | [[package]] 2159 | name = "zerovec" 2160 | version = "0.10.4" 2161 | source = "registry+https://github.com/rust-lang/crates.io-index" 2162 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 2163 | dependencies = [ 2164 | "yoke", 2165 | "zerofrom", 2166 | "zerovec-derive", 2167 | ] 2168 | 2169 | [[package]] 2170 | name = "zerovec-derive" 2171 | version = "0.10.3" 2172 | source = "registry+https://github.com/rust-lang/crates.io-index" 2173 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 2174 | dependencies = [ 2175 | "proc-macro2", 2176 | "quote", 2177 | "syn", 2178 | ] 2179 | 2180 | [[package]] 2181 | name = "zstd" 2182 | version = "0.13.3" 2183 | source = "registry+https://github.com/rust-lang/crates.io-index" 2184 | checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 2185 | dependencies = [ 2186 | "zstd-safe", 2187 | ] 2188 | 2189 | [[package]] 2190 | name = "zstd-safe" 2191 | version = "7.2.4" 2192 | source = "registry+https://github.com/rust-lang/crates.io-index" 2193 | checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 2194 | dependencies = [ 2195 | "zstd-sys", 2196 | ] 2197 | 2198 | [[package]] 2199 | name = "zstd-sys" 2200 | version = "2.0.15+zstd.1.5.7" 2201 | source = "registry+https://github.com/rust-lang/crates.io-index" 2202 | checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" 2203 | dependencies = [ 2204 | "cc", 2205 | "pkg-config", 2206 | ] 2207 | --------------------------------------------------------------------------------