From 55f2060664b3ba049c8a1981ada36b6b3ba8fe60 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 10:37:17 +0800 Subject: [PATCH 01/12] feat(client): add x-b3-traceid and x-xray-traceid generation methods --- src/xhshow/client.py | 36 ++++++++++++++++++++++++++++++ src/xhshow/config/config.py | 9 ++++++++ src/xhshow/utils/random_gen.py | 40 +++++++++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index e813e72..8529c9c 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -4,6 +4,7 @@ from .config import CryptoConfig from .core.crypto import CryptoProcessor +from .utils.random_gen import RandomGenerator from .utils.url_utils import build_url, extract_uri from .utils.validators import ( validate_get_signature_params, @@ -20,6 +21,7 @@ class Xhshow: def __init__(self, config: CryptoConfig | None = None): self.config = config or CryptoConfig() self.crypto_processor = CryptoProcessor(self.config) + self.random_generator = RandomGenerator() def _build_content_string(self, method: str, uri: str, payload: dict[str, Any] | None = None) -> str: """ @@ -264,3 +266,37 @@ def build_json_body(self, payload: dict[str, Any]) -> str: '{"username":"test","password":"123456"}' """ return json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + + def get_b3_trace_id(self) -> str: + """ + Generate x-b3-traceid for HTTP request headers + + Returns: + str: 16-character hexadecimal trace ID + + Examples: + >>> client = Xhshow() + >>> client.get_b3_trace_id() + '63cd207ddeb2e360' + """ + return self.random_generator.generate_b3_trace_id() + + def get_xray_trace_id(self, timestamp: int | None = None, seq: int | None = None) -> str: + """ + Generate x-xray-traceid for HTTP request headers + + Args: + timestamp: Unix timestamp in milliseconds (defaults to current time) + seq: Sequence number 0 to 2^23-1 (defaults to random value) + + Returns: + str: 32-character hexadecimal trace ID + + Examples: + >>> client = Xhshow() + >>> client.get_xray_trace_id() + 'cd7621e82d9c24e90bfd937d92bbbd1b' + >>> client.get_xray_trace_id(timestamp=1764896636081, seq=5) + 'cd7604be588000051a7fb8ae74496a76' + """ + return self.random_generator.generate_xray_trace_id(timestamp, seq) diff --git a/src/xhshow/config/config.py b/src/xhshow/config/config.py index 7af6954..6fcf380 100644 --- a/src/xhshow/config/config.py +++ b/src/xhshow/config/config.py @@ -11,6 +11,7 @@ class CryptoConfig: # Bitwise operation constants MAX_32BIT: int = 0xFFFFFFFF MAX_SIGNED_32BIT: int = 0x7FFFFFFF + MAX_BYTE: int = 255 # Base64 encoding constants STANDARD_BASE64_ALPHABET: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" @@ -61,6 +62,14 @@ class CryptoConfig: X3_PREFIX: str = "mns0301_" XYS_PREFIX: str = "XYS_" + # Trace ID generation constants + HEX_CHARS: str = "abcdef0123456789" + XRAY_TRACE_ID_SEQ_MAX: int = 8388607 # 2^23-1 + XRAY_TRACE_ID_TIMESTAMP_SHIFT: int = 23 + XRAY_TRACE_ID_PART1_LENGTH: int = 16 + XRAY_TRACE_ID_PART2_LENGTH: int = 16 + B3_TRACE_ID_LENGTH: int = 16 + def with_overrides(self, **kwargs: Any) -> "CryptoConfig": """ Create a new config instance with overridden values diff --git a/src/xhshow/utils/random_gen.py b/src/xhshow/utils/random_gen.py index a6965c6..6a7705b 100644 --- a/src/xhshow/utils/random_gen.py +++ b/src/xhshow/utils/random_gen.py @@ -1,4 +1,5 @@ import random +import time from ..config import CryptoConfig @@ -21,7 +22,7 @@ def generate_random_bytes(self, byte_count: int) -> list[int]: Returns: list[int]: Random byte array """ - return [random.randint(0, 255) for _ in range(byte_count)] + return [random.randint(0, self.config.MAX_BYTE) for _ in range(byte_count)] def generate_random_byte_in_range(self, min_val: int, max_val: int) -> int: """ @@ -44,3 +45,40 @@ def generate_random_int(self) -> int: int: Random 32-bit integer """ return random.randint(0, self.config.MAX_32BIT) + + def generate_b3_trace_id(self) -> str: + """ + Generate x-b3-traceid (16 random hex characters) + + Returns: + str: 16-character hexadecimal trace ID + """ + return "".join(random.choice(self.config.HEX_CHARS) for _ in range(self.config.B3_TRACE_ID_LENGTH)) + + def generate_xray_trace_id(self, timestamp: int | None = None, seq: int | None = None) -> str: + """ + Generate x-xray-traceid (32 characters: 16 timestamp+seq + 16 random) + + Args: + timestamp: Unix timestamp in milliseconds (defaults to current time) + seq: Sequence number 0 to 2^23-1 (defaults to random value) + + Returns: + str: 32-character hexadecimal trace ID + """ + if timestamp is None: + timestamp = int(time.time() * 1000) + if seq is None: + seq = random.randint(0, self.config.XRAY_TRACE_ID_SEQ_MAX) + + # First 16 chars: XHS xray parameter uses timestamp bit operations + part1 = format( + ((timestamp << self.config.XRAY_TRACE_ID_TIMESTAMP_SHIFT) | seq), + f"0{self.config.XRAY_TRACE_ID_PART1_LENGTH}x", + ) + # Last 16 chars: completely random, untraceable, can be simplified + part2 = "".join( + random.choice(self.config.HEX_CHARS) for _ in range(self.config.XRAY_TRACE_ID_PART2_LENGTH) + ) + + return part1 + part2 From f19b51343067ae0799c599bd9f872a9cf04727e6 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 10:52:42 +0800 Subject: [PATCH 02/12] feat(client): add trace id generation and unified timestamp support --- src/xhshow/client.py | 40 +++++++++-- src/xhshow/core/crypto.py | 5 +- src/xhshow/utils/validators.py | 6 ++ tests/test_crypto.py | 122 +++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 5 deletions(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index 8529c9c..253edec 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -1,5 +1,6 @@ import hashlib import json +import time from typing import Any, Literal from .config import CryptoConfig @@ -69,6 +70,7 @@ def _build_signature( a1_value: str, xsec_appid: str = "xhs-pc-web", string_param: str = "", + timestamp: float | None = None, ) -> str: """ Build signature @@ -78,11 +80,14 @@ def _build_signature( a1_value: a1 value from cookies xsec_appid: Application identifier string_param: String parameter + timestamp: Unix timestamp in seconds (defaults to current time) Returns: str: Base64 encoded signature """ - payload_array = self.crypto_processor.build_payload_array(d_value, a1_value, xsec_appid, string_param) + payload_array = self.crypto_processor.build_payload_array( + d_value, a1_value, xsec_appid, string_param, timestamp + ) xor_result = self.crypto_processor.bit_ops.xor_transform_array(payload_array) @@ -96,6 +101,7 @@ def sign_xs( a1_value: str, xsec_appid: str = "xhs-pc-web", payload: dict[str, Any] | None = None, + timestamp: float | None = None, ) -> str: """ Generate request signature (supports GET and POST) @@ -111,6 +117,7 @@ def sign_xs( payload: Request parameters - GET request: params value - POST request: payload value + timestamp: Unix timestamp in seconds (defaults to current time) Returns: str: Complete signature string @@ -127,7 +134,7 @@ def sign_xs( d_value = self._generate_d_value(content_string) signature_data["x3"] = self.crypto_processor.config.X3_PREFIX + self._build_signature( - d_value, a1_value, xsec_appid, content_string + d_value, a1_value, xsec_appid, content_string, timestamp ) return self.crypto_processor.config.XYS_PREFIX + self.crypto_processor.b64encoder.encode( json.dumps(signature_data, separators=(",", ":"), ensure_ascii=False) @@ -140,6 +147,7 @@ def sign_xs_get( a1_value: str, xsec_appid: str = "xhs-pc-web", params: dict[str, Any] | None = None, + timestamp: float | None = None, ) -> str: """ Generate GET request signature (convenience method) @@ -151,6 +159,7 @@ def sign_xs_get( a1_value: a1 value from cookies xsec_appid: Application identifier, defaults to `xhs-pc-web` params: GET request parameters + timestamp: Unix timestamp in seconds (defaults to current time) Returns: str: Complete signature string @@ -159,7 +168,7 @@ def sign_xs_get( TypeError: Parameter type error ValueError: Parameter value error """ - return self.sign_xs("GET", uri, a1_value, xsec_appid, params) + return self.sign_xs("GET", uri, a1_value, xsec_appid, payload=params, timestamp=timestamp) @validate_post_signature_params def sign_xs_post( @@ -168,6 +177,7 @@ def sign_xs_post( a1_value: str, xsec_appid: str = "xhs-pc-web", payload: dict[str, Any] | None = None, + timestamp: float | None = None, ) -> str: """ Generate POST request signature (convenience method) @@ -179,6 +189,7 @@ def sign_xs_post( a1_value: a1 value from cookies xsec_appid: Application identifier, defaults to `xhs-pc-web` payload: POST request body data + timestamp: Unix timestamp in seconds (defaults to current time) Returns: str: Complete signature string @@ -187,7 +198,7 @@ def sign_xs_post( TypeError: Parameter type error ValueError: Parameter value error """ - return self.sign_xs("POST", uri, a1_value, xsec_appid, payload) + return self.sign_xs("POST", uri, a1_value, xsec_appid, payload=payload, timestamp=timestamp) def decode_x3(self, x3_signature: str) -> bytearray: """ @@ -300,3 +311,24 @@ def get_xray_trace_id(self, timestamp: int | None = None, seq: int | None = None 'cd7604be588000051a7fb8ae74496a76' """ return self.random_generator.generate_xray_trace_id(timestamp, seq) + + def get_x_t(self, timestamp: float | None = None) -> int: + """ + Generate x-t header value (Unix timestamp in milliseconds) + + Args: + timestamp: Unix timestamp in seconds (defaults to current time) + + Returns: + int: Unix timestamp in milliseconds + + Examples: + >>> client = Xhshow() + >>> client.get_x_t() + 1764902784843 + >>> client.get_x_t(timestamp=1764896636.081) + 1764896636081 + """ + if timestamp is None: + timestamp = time.time() + return int(timestamp * 1000) diff --git a/src/xhshow/core/crypto.py b/src/xhshow/core/crypto.py index 7693cd2..8d3be5e 100644 --- a/src/xhshow/core/crypto.py +++ b/src/xhshow/core/crypto.py @@ -56,6 +56,7 @@ def build_payload_array( a1_value: str, app_identifier: str = "xhs-pc-web", string_param: str = "", + timestamp: float | None = None, ) -> list[int]: """ Build payload array (t.js version - exact match) @@ -65,6 +66,7 @@ def build_payload_array( a1_value (str): a1 value from cookies app_identifier (str): Application identifier, default "xhs-pc-web" string_param (str): String parameter (used for URI length calculation) + timestamp (float | None): Unix timestamp in seconds (defaults to current time) Returns: list[int]: Complete payload byte array (124 bytes) @@ -78,7 +80,8 @@ def build_payload_array( payload.extend(seed_bytes) seed_byte_0 = seed_bytes[0] - timestamp = time.time() + if timestamp is None: + timestamp = time.time() payload.extend(self.env_fingerprint_a(int(timestamp * 1000), self.config.ENV_FINGERPRINT_XOR_KEY)) time_offset = self.random_gen.generate_random_byte_in_range( diff --git a/src/xhshow/utils/validators.py b/src/xhshow/utils/validators.py index d585b1a..82ad3d1 100644 --- a/src/xhshow/utils/validators.py +++ b/src/xhshow/utils/validators.py @@ -96,6 +96,7 @@ def wrapper( a1_value: Any, xsec_appid: Any = "xhs-pc-web", payload: Any = None, + timestamp: float | None = None, ): validator = RequestSignatureValidator() @@ -112,6 +113,7 @@ def wrapper( validated_a1_value, validated_xsec_appid, validated_payload, + timestamp, ) return wrapper # type: ignore @@ -135,6 +137,7 @@ def wrapper( a1_value: Any, xsec_appid: Any = "xhs-pc-web", params: Any = None, + timestamp: float | None = None, ): validator = RequestSignatureValidator() @@ -149,6 +152,7 @@ def wrapper( validated_a1_value, validated_xsec_appid, validated_params, + timestamp, ) return wrapper # type: ignore @@ -172,6 +176,7 @@ def wrapper( a1_value: Any, xsec_appid: Any = "xhs-pc-web", payload: Any = None, + timestamp: float | None = None, ): validator = RequestSignatureValidator() @@ -186,6 +191,7 @@ def wrapper( validated_a1_value, validated_xsec_appid, validated_payload, + timestamp, ) return wrapper # type: ignore diff --git a/tests/test_crypto.py b/tests/test_crypto.py index dda704d..021964f 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -387,3 +387,125 @@ def test_sign_xs_parameter_validation(self): result = client.sign_xs(" get ", " /api/test ", " test_a1 ") # type: ignore assert isinstance(result, str) assert result.startswith("XYS_") + + def test_timestamp_parameter_support(self): + """测试时间戳参数支持""" + import time + + client = Xhshow() + uri = "/api/sns/web/v1/user_posted" + a1_value = "test_a1_value" + params = {"num": "30"} + + # Test with default timestamp + sig1 = client.sign_xs_get(uri, a1_value, params=params) + assert isinstance(sig1, str) + assert sig1.startswith("XYS_") + + # Test with custom timestamp + custom_ts = time.time() + sig2 = client.sign_xs_get(uri, a1_value, params=params, timestamp=custom_ts) + assert isinstance(sig2, str) + assert sig2.startswith("XYS_") + + # Test POST with timestamp + sig3 = client.sign_xs_post( + uri="/api/sns/web/v1/login", + a1_value=a1_value, + payload={"user": "test"}, + timestamp=custom_ts, + ) + assert isinstance(sig3, str) + assert sig3.startswith("XYS_") + + # Test sign_xs with timestamp + sig4 = client.sign_xs(method="GET", uri=uri, a1_value=a1_value, payload=params, timestamp=custom_ts) + assert isinstance(sig4, str) + assert sig4.startswith("XYS_") + + def test_trace_id_generation(self): + """测试 Trace ID 生成""" + import time + + client = Xhshow() + + # Test b3 trace id + b3_id = client.get_b3_trace_id() + assert isinstance(b3_id, str) + assert len(b3_id) == 16 + assert all(c in "0123456789abcdef" for c in b3_id) + + # Test xray trace id with default timestamp + xray_id1 = client.get_xray_trace_id() + assert isinstance(xray_id1, str) + assert len(xray_id1) == 32 + assert all(c in "0123456789abcdef" for c in xray_id1) + + # Test xray trace id with custom timestamp + custom_ts = int(time.time() * 1000) + xray_id2 = client.get_xray_trace_id(timestamp=custom_ts) + assert isinstance(xray_id2, str) + assert len(xray_id2) == 32 + + # Test xray trace id with custom timestamp and seq + xray_id3 = client.get_xray_trace_id(timestamp=custom_ts, seq=12345) + assert isinstance(xray_id3, str) + assert len(xray_id3) == 32 + + def test_unified_timestamp_usage(self): + """测试统一时间戳使用场景""" + import time + + client = Xhshow() + unified_ts = time.time() + + # Use same timestamp for signature and trace IDs + signature = client.sign_xs_post( + uri="/api/sns/web/v1/login", + a1_value="test_a1", + payload={"user": "test"}, + timestamp=unified_ts, + ) + + b3_id = client.get_b3_trace_id() + xray_id = client.get_xray_trace_id(timestamp=int(unified_ts * 1000)) + + assert isinstance(signature, str) + assert signature.startswith("XYS_") + assert isinstance(b3_id, str) + assert len(b3_id) == 16 + assert isinstance(xray_id, str) + assert len(xray_id) == 32 + + def test_get_x_t(self): + """测试 x-t header 生成""" + import time + + client = Xhshow() + + # Test with default timestamp + x_t1 = client.get_x_t() + assert isinstance(x_t1, int) + assert x_t1 > 0 + + # Test with custom timestamp + custom_ts = time.time() + x_t2 = client.get_x_t(timestamp=custom_ts) + assert isinstance(x_t2, int) + assert x_t2 == int(custom_ts * 1000) + + # Test unified timestamp with all headers + unified_ts = time.time() + x_t = client.get_x_t(timestamp=unified_ts) + signature = client.sign_xs_get( + uri="/api/test", a1_value="test_a1", params={"key": "value"}, timestamp=unified_ts + ) + b3_id = client.get_b3_trace_id() + xray_id = client.get_xray_trace_id(timestamp=int(unified_ts * 1000)) + + assert isinstance(x_t, int) + assert x_t == int(unified_ts * 1000) + assert isinstance(signature, str) + assert isinstance(b3_id, str) + assert isinstance(xray_id, str) + From cfc809a29b696dc02a40236e418ffdd7d19a4574 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 10:57:16 +0800 Subject: [PATCH 03/12] feat(client): add unified sign_headers method for complete request headers --- src/xhshow/client.py | 49 ++++++++++++++++++++++++++++++++++++++++ tests/test_crypto.py | 53 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index 253edec..bbe25b8 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -332,3 +332,52 @@ def get_x_t(self, timestamp: float | None = None) -> int: if timestamp is None: timestamp = time.time() return int(timestamp * 1000) + + def sign_headers( + self, + method: Literal["GET", "POST"], + uri: str, + a1_value: str, + xsec_appid: str = "xhs-pc-web", + payload: dict[str, Any] | None = None, + timestamp: float | None = None, + ) -> dict[str, str]: + """ + Generate complete request headers with signature and trace IDs + + Args: + method: Request method ("GET" or "POST") + uri: Request URI or full URL + a1_value: a1 value from cookies + xsec_appid: Application identifier, defaults to `xhs-pc-web` + payload: Request parameters (GET: params, POST: payload) + timestamp: Unix timestamp in seconds (defaults to current time) + + Returns: + dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + + Examples: + >>> client = Xhshow() + >>> headers = client.sign_headers( + ... method="GET", + ... uri="/api/sns/web/v1/user_posted", + ... a1_value="your_a1_value", + ... payload={"num": "30"} + ... ) + >>> headers.keys() + dict_keys(['x-s', 'x-t', 'x-b3-traceid', 'x-xray-traceid']) + """ + if timestamp is None: + timestamp = time.time() + + x_s = self.sign_xs(method, uri, a1_value, xsec_appid, payload, timestamp) + x_t = self.get_x_t(timestamp) + x_b3_traceid = self.get_b3_trace_id() + x_xray_traceid = self.get_xray_trace_id(timestamp=int(timestamp * 1000)) + + return { + "x-s": x_s, + "x-t": str(x_t), + "x-b3-traceid": x_b3_traceid, + "x-xray-traceid": x_xray_traceid, + } diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 021964f..14a5cf1 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -509,3 +509,56 @@ def test_get_x_t(self): assert isinstance(b3_id, str) assert isinstance(xray_id, str) + def test_sign_headers(self): + """测试统一 headers 生成""" + import time + + client = Xhshow() + + # Test GET request headers + headers = client.sign_headers( + method="GET", + uri="/api/sns/web/v1/user_posted", + a1_value="test_a1_value", + payload={"num": "30", "cursor": "", "user_id": "123"}, + ) + + assert isinstance(headers, dict) + assert "x-s" in headers + assert "x-t" in headers + assert "x-b3-traceid" in headers + assert "x-xray-traceid" in headers + + assert headers["x-s"].startswith("XYS_") + assert headers["x-t"].isdigit() + assert len(headers["x-b3-traceid"]) == 16 + assert len(headers["x-xray-traceid"]) == 32 + + # Test POST request headers + headers_post = client.sign_headers( + method="POST", + uri="/api/sns/web/v1/login", + a1_value="test_a1_value", + payload={"username": "test", "password": "123456"}, + ) + + assert isinstance(headers_post, dict) + assert all(k in headers_post for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + + # Test with custom timestamp + custom_ts = time.time() + headers_custom = client.sign_headers( + method="GET", + uri="/api/test", + a1_value="test_a1", + payload={"key": "value"}, + timestamp=custom_ts, + ) + + assert isinstance(headers_custom, dict) + assert headers_custom["x-t"] == str(int(custom_ts * 1000)) + + # Test all values are strings + assert all(isinstance(v, str) for v in headers.values()) + assert all(isinstance(v, str) for v in headers_post.values()) + assert all(isinstance(v, str) for v in headers_custom.values()) From d50d50c17a27f81349cadf05367a56cbef43267d Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 11:35:37 +0800 Subject: [PATCH 04/12] feat(client): improve sign_headers parameter handling and add convenience methods - fix: distinguish params/payload parameters in sign_headers method - params: for GET requests only - payload: for POST requests only - feat: add sign_headers_get convenience method for GET requests - feat: add sign_headers_post convenience method for POST requests - docs: update docstrings with clear parameter usage examples --- src/xhshow/client.py | 81 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index bbe25b8..ac63cac 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -339,6 +339,7 @@ def sign_headers( uri: str, a1_value: str, xsec_appid: str = "xhs-pc-web", + params: dict[str, Any] | None = None, payload: dict[str, Any] | None = None, timestamp: float | None = None, ) -> dict[str, str]: @@ -350,7 +351,8 @@ def sign_headers( uri: Request URI or full URL a1_value: a1 value from cookies xsec_appid: Application identifier, defaults to `xhs-pc-web` - payload: Request parameters (GET: params, POST: payload) + params: GET request parameters (only used when method="GET") + payload: POST request body data (only used when method="POST") timestamp: Unix timestamp in seconds (defaults to current time) Returns: @@ -358,11 +360,19 @@ def sign_headers( Examples: >>> client = Xhshow() + >>> # GET request >>> headers = client.sign_headers( ... method="GET", ... uri="/api/sns/web/v1/user_posted", ... a1_value="your_a1_value", - ... payload={"num": "30"} + ... params={"num": "30"} + ... ) + >>> # POST request + >>> headers = client.sign_headers( + ... method="POST", + ... uri="/api/sns/web/v1/login", + ... a1_value="your_a1_value", + ... payload={"username": "test"} ... ) >>> headers.keys() dict_keys(['x-s', 'x-t', 'x-b3-traceid', 'x-xray-traceid']) @@ -370,7 +380,10 @@ def sign_headers( if timestamp is None: timestamp = time.time() - x_s = self.sign_xs(method, uri, a1_value, xsec_appid, payload, timestamp) + # Use params for GET, payload for POST + request_data = params if method.upper() == "GET" else payload + + x_s = self.sign_xs(method, uri, a1_value, xsec_appid, request_data, timestamp) x_t = self.get_x_t(timestamp) x_b3_traceid = self.get_b3_trace_id() x_xray_traceid = self.get_xray_trace_id(timestamp=int(timestamp * 1000)) @@ -381,3 +394,65 @@ def sign_headers( "x-b3-traceid": x_b3_traceid, "x-xray-traceid": x_xray_traceid, } + + def sign_headers_get( + self, + uri: str, + a1_value: str, + xsec_appid: str = "xhs-pc-web", + params: dict[str, Any] | None = None, + timestamp: float | None = None, + ) -> dict[str, str]: + """ + Generate complete request headers for GET request (convenience method) + + Args: + uri: Request URI or full URL + a1_value: a1 value from cookies + xsec_appid: Application identifier, defaults to `xhs-pc-web` + params: GET request parameters + timestamp: Unix timestamp in seconds (defaults to current time) + + Returns: + dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + + Examples: + >>> client = Xhshow() + >>> headers = client.sign_headers_get( + ... uri="/api/sns/web/v1/user_posted", + ... a1_value="your_a1_value", + ... params={"num": "30"} + ... ) + """ + return self.sign_headers("GET", uri, a1_value, xsec_appid, params=params, timestamp=timestamp) + + def sign_headers_post( + self, + uri: str, + a1_value: str, + xsec_appid: str = "xhs-pc-web", + payload: dict[str, Any] | None = None, + timestamp: float | None = None, + ) -> dict[str, str]: + """ + Generate complete request headers for POST request (convenience method) + + Args: + uri: Request URI or full URL + a1_value: a1 value from cookies + xsec_appid: Application identifier, defaults to `xhs-pc-web` + payload: POST request body data + timestamp: Unix timestamp in seconds (defaults to current time) + + Returns: + dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + + Examples: + >>> client = Xhshow() + >>> headers = client.sign_headers_post( + ... uri="/api/sns/web/v1/login", + ... a1_value="your_a1_value", + ... payload={"username": "test", "password": "123456"} + ... ) + """ + return self.sign_headers("POST", uri, a1_value, xsec_appid, payload=payload, timestamp=timestamp) From a3a77169b47bacd55533359d4f1eab356c3d9aee Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 11:35:56 +0800 Subject: [PATCH 05/12] test(crypto): update tests for improved sign_headers methods - fix: use params for GET and payload for POST in sign_headers tests - test: add test_sign_headers_get for GET convenience method - test: add test_sign_headers_post for POST convenience method - test: verify timestamp parameter support in convenience methods --- tests/test_crypto.py | 60 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 14a5cf1..2053170 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -515,12 +515,12 @@ def test_sign_headers(self): client = Xhshow() - # Test GET request headers + # Test GET request headers with params headers = client.sign_headers( method="GET", uri="/api/sns/web/v1/user_posted", a1_value="test_a1_value", - payload={"num": "30", "cursor": "", "user_id": "123"}, + params={"num": "30", "cursor": "", "user_id": "123"}, ) assert isinstance(headers, dict) @@ -534,7 +534,7 @@ def test_sign_headers(self): assert len(headers["x-b3-traceid"]) == 16 assert len(headers["x-xray-traceid"]) == 32 - # Test POST request headers + # Test POST request headers with payload headers_post = client.sign_headers( method="POST", uri="/api/sns/web/v1/login", @@ -551,7 +551,7 @@ def test_sign_headers(self): method="GET", uri="/api/test", a1_value="test_a1", - payload={"key": "value"}, + params={"key": "value"}, timestamp=custom_ts, ) @@ -562,3 +562,55 @@ def test_sign_headers(self): assert all(isinstance(v, str) for v in headers.values()) assert all(isinstance(v, str) for v in headers_post.values()) assert all(isinstance(v, str) for v in headers_custom.values()) + + def test_sign_headers_get(self): + """测试 GET 请求 headers 便捷方法""" + import time + + client = Xhshow() + + # Test basic usage + headers = client.sign_headers_get( + uri="/api/sns/web/v1/user_posted", + a1_value="test_a1_value", + params={"num": "30", "cursor": "", "user_id": "123"}, + ) + + assert isinstance(headers, dict) + assert all(k in headers for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert headers["x-s"].startswith("XYS_") + assert all(isinstance(v, str) for v in headers.values()) + + # Test with custom timestamp + custom_ts = time.time() + headers_ts = client.sign_headers_get( + uri="/api/test", a1_value="test_a1", params={"key": "value"}, timestamp=custom_ts + ) + + assert headers_ts["x-t"] == str(int(custom_ts * 1000)) + + def test_sign_headers_post(self): + """测试 POST 请求 headers 便捷方法""" + import time + + client = Xhshow() + + # Test basic usage + headers = client.sign_headers_post( + uri="/api/sns/web/v1/login", + a1_value="test_a1_value", + payload={"username": "test", "password": "123456"}, + ) + + assert isinstance(headers, dict) + assert all(k in headers for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert headers["x-s"].startswith("XYS_") + assert all(isinstance(v, str) for v in headers.values()) + + # Test with custom timestamp + custom_ts = time.time() + headers_ts = client.sign_headers_post( + uri="/api/test", a1_value="test_a1", payload={"key": "value"}, timestamp=custom_ts + ) + + assert headers_ts["x-t"] == str(int(custom_ts * 1000)) From b0902eda20692e79814247dac5cfa6e1ae6a51cb Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 11:36:07 +0800 Subject: [PATCH 06/12] docs(readme): reorganize usage examples to highlight convenience methods - feat: promote sign_headers_get/post as recommended approach - feat: move build_url and build_json_body to recommended section - refactor: move sign_headers unified method to traditional methods section - refactor: collapse traditional single-field generation methods into details - improve: clearer documentation structure for better user experience --- README.md | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a102dcb..4b2b513 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ pip install xhshow ## 使用方法 -### 基本用法 +### 基本用法(推荐) ```python from xhshow import Xhshow @@ -31,21 +31,53 @@ import requests client = Xhshow() -# GET请求签名 -signature = client.sign_xs_get( - uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", # v0.1.3及后续版本支持自动提取uri - # uri="/api/sns/web/v1/user_posted" # v0.1.2及以前版本需要主动提取uri +# 注意: uri 参数可以传递完整 URL 或 URI 路径,会自动提取 URI +headers = client.sign_headers_get( + uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", # 完整 URL(推荐) + # uri="/api/sns/web/v1/user_posted", # 或者只传 URI 路径 a1_value="your_a1_cookie_value", params={"num": "30", "cursor": "", "user_id": "123"} ) -# POST请求签名 -signature = client.sign_xs_post( +# 方式1: 使用 update 方法更新现有 headers(推荐) +base_headers = { + "User-Agent": "Mozilla/5.0...", + "Content-Type": "application/json" +} +base_headers.update(headers) +response = requests.get( + "https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", + params={"num": "30", "cursor": "", "user_id": "123"}, + headers=base_headers, + cookies={"a1": "your_a1_cookie_value"} +) + +# 方式2: 使用 ** 解包创建新 headers +response = requests.get( + "https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", + params={"num": "30", "cursor": "", "user_id": "123"}, + headers={ + "User-Agent": "Mozilla/5.0...", + "Content-Type": "application/json", + **headers # 解包签名 headers(会创建新字典) + }, + cookies={"a1": "your_a1_cookie_value"} +) + +# POST 请求示例:使用 sign_headers_post +headers_post = client.sign_headers_post( uri="https://edith.xiaohongshu.com/api/sns/web/v1/login", a1_value="your_a1_cookie_value", payload={"username": "test", "password": "123456"} ) +response = requests.post( + "https://edith.xiaohongshu.com/api/sns/web/v1/login", + json={"username": "test", "password": "123456"}, + headers={**base_headers, **headers_post}, + cookies={"a1": "your_a1_cookie_value"} +) + # 构建符合xhs平台的GET请求链接 full_url = client.build_url( base_url="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", @@ -60,6 +92,67 @@ json_body = client.build_json_body( response = requests.post(url, data=json_body, headers=headers, cookies=cookies) ``` +
+传统方法(单独生成各个字段) + +```python +from xhshow import Xhshow +import requests + +client = Xhshow() + +# 也可以使用统一方法 sign_headers(需要手动指定 method) +headers = client.sign_headers( + method="GET", # 或 "POST" + uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", + a1_value="your_a1_cookie_value", + params={"num": "30"} # GET 请求使用 params,POST 请求使用 payload +) + +# GET 请求签名 +x_s = client.sign_xs_get( + uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", # v0.1.3及后续版本支持自动提取uri + # uri="/api/sns/web/v1/user_posted" # v0.1.2及以前版本需要主动提取uri + a1_value="your_a1_cookie_value", + params={"num": "30", "cursor": "", "user_id": "123"} +) + +# POST 请求签名 +x_s = client.sign_xs_post( + uri="https://edith.xiaohongshu.com/api/sns/web/v1/login", + a1_value="your_a1_cookie_value", + payload={"username": "test", "password": "123456"} +) + +# 生成其他 headers 字段 +x_t = client.get_x_t() # 时间戳(毫秒) +x_b3_traceid = client.get_b3_trace_id() # 16位随机 trace id +x_xray_traceid = client.get_xray_trace_id() # 32位 trace id + +# 手动构建 headers +headers = { + "x-s": x_s, + "x-t": str(x_t), + "x-b3-traceid": x_b3_traceid, + "x-xray-traceid": x_xray_traceid +} + +# 使用统一时间戳(可选,确保所有字段使用相同时间) +import time +timestamp = time.time() + +x_s = client.sign_xs_get( + uri="/api/sns/web/v1/user_posted", + a1_value="your_a1_cookie_value", + params={"num": "30"}, + timestamp=timestamp # 传入统一时间戳 +) +x_t = client.get_x_t(timestamp=timestamp) +x_xray_traceid = client.get_xray_trace_id(timestamp=int(timestamp * 1000)) +``` + +
+ ### 解密签名 ```python From b95df9631ec69e2025413bd25b874cabdfb27ad7 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 12:11:47 +0800 Subject: [PATCH 07/12] fuck style --- src/xhshow/utils/random_gen.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/xhshow/utils/random_gen.py b/src/xhshow/utils/random_gen.py index 6a7705b..0359cc9 100644 --- a/src/xhshow/utils/random_gen.py +++ b/src/xhshow/utils/random_gen.py @@ -77,8 +77,6 @@ def generate_xray_trace_id(self, timestamp: int | None = None, seq: int | None = f"0{self.config.XRAY_TRACE_ID_PART1_LENGTH}x", ) # Last 16 chars: completely random, untraceable, can be simplified - part2 = "".join( - random.choice(self.config.HEX_CHARS) for _ in range(self.config.XRAY_TRACE_ID_PART2_LENGTH) - ) + part2 = "".join(random.choice(self.config.HEX_CHARS) for _ in range(self.config.XRAY_TRACE_ID_PART2_LENGTH)) return part1 + part2 From 9ed3ebaf70d9d1eeb790892ae65734723831da73 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Fri, 5 Dec 2025 12:17:29 +0800 Subject: [PATCH 08/12] feat(client): add parameter validation for sign_headers method - validate GET requests only use params, not payload - validate POST requests only use payload, not params - raise ValueError for unsupported HTTP methods (only GET/POST allowed) - add comprehensive test coverage for parameter validation - improve error messages for better developer experience --- src/xhshow/client.py | 17 ++++++++++++++--- tests/test_crypto.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index ac63cac..977a4bb 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -380,10 +380,21 @@ def sign_headers( if timestamp is None: timestamp = time.time() - # Use params for GET, payload for POST - request_data = params if method.upper() == "GET" else payload + method_upper = method.upper() + + # Validate method and parameters + if method_upper == "GET": + if payload is not None: + raise ValueError("GET requests must use 'params', not 'payload'") + request_data = params + elif method_upper == "POST": + if params is not None: + raise ValueError("POST requests must use 'payload', not 'params'") + request_data = payload + else: + raise ValueError(f"Unsupported method: {method}") - x_s = self.sign_xs(method, uri, a1_value, xsec_appid, request_data, timestamp) + x_s = self.sign_xs(method_upper, uri, a1_value, xsec_appid, request_data, timestamp) x_t = self.get_x_t(timestamp) x_b3_traceid = self.get_b3_trace_id() x_xray_traceid = self.get_xray_trace_id(timestamp=int(timestamp * 1000)) diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 2053170..70be442 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -614,3 +614,34 @@ def test_sign_headers_post(self): ) assert headers_ts["x-t"] == str(int(custom_ts * 1000)) + + def test_sign_headers_parameter_validation(self): + """测试 sign_headers 参数验证""" + client = Xhshow() + + # Test GET request with payload should raise error + with pytest.raises(ValueError, match="GET requests must use 'params', not 'payload'"): + client.sign_headers( + method="GET", + uri="/api/test", + a1_value="test_a1", + payload={"key": "value"}, + ) + + # Test POST request with params should raise error + with pytest.raises(ValueError, match="POST requests must use 'payload', not 'params'"): + client.sign_headers( + method="POST", + uri="/api/test", + a1_value="test_a1", + params={"key": "value"}, + ) + + # Test unsupported method should raise error + with pytest.raises(ValueError, match="Unsupported method"): + client.sign_headers( + method="PUT", + uri="/api/test", + a1_value="test_a1", + params={"key": "value"}, + ) From f9c8c076d6a337e7b854a01989ade034376e5b58 Mon Sep 17 00:00:00 2001 From: invisibility <56670709+illusiona@users.noreply.github.com> Date: Sun, 7 Dec 2025 02:07:20 +0800 Subject: [PATCH 09/12] add xs-common parameter (#72) * Add the cryptodemo library, using a custom encode_to_b64 method instead of the official standard base64 method. Add some initialization parameters to the configuration file to generate fingerprints. Perhaps using customer_encoder to replace the encoder file name is better than directly using the encoder name, since encoders are a native Python method. * Add the cryptodemo library, using a custom encode_to_b64 method instead of the official standard base64 method. Add some initialization parameters to the configuration file to generate fingerprints. ADD CRC32_encrypt for gen xs-common Perhaps using customer_encoder to replace the encoder file name is better than directly using the encoder name, since encoders are a native Python method. Use case: cookie = "you cookie dict or string" xs_common = client.sign_xsc(cookie) * style: format code to match project standards * refactor(core): split fingerprint generation into separate modules * perf(encoder): optimize base64 encoding with cached translation tables * feat(client): add unified cookie parsing and x-s-common signature support * test: add comprehensive cookie parsing tests and update crypto tests * chore(crc32): remove example code comments * docs: update API documentation for cookie-based authentication * chore(deps): update project dependencies --------- Co-authored-by: Cloxl --- README.md | 89 +++- pyproject.toml | 5 +- src/xhshow/client.py | 102 ++++- src/xhshow/config/config.py | 37 +- src/xhshow/core/common_sign.py | 49 +++ src/xhshow/core/crc32_encrypt.py | 124 ++++++ src/xhshow/data/__init__.py | 25 ++ src/xhshow/data/fingerprint_data.py | 133 ++++++ src/xhshow/generators/__init__.py | 5 + src/xhshow/generators/fingerprint.py | 217 ++++++++++ src/xhshow/generators/fingerprint_helpers.py | 92 ++++ src/xhshow/utils/encoder.py | 33 +- src/xhshow/utils/validators.py | 45 ++ tests/test_cookie_parsing.py | 425 +++++++++++++++++++ tests/test_crypto.py | 85 +++- uv.lock | 162 +++++++ 16 files changed, 1570 insertions(+), 58 deletions(-) create mode 100644 src/xhshow/core/common_sign.py create mode 100644 src/xhshow/core/crc32_encrypt.py create mode 100644 src/xhshow/data/__init__.py create mode 100644 src/xhshow/data/fingerprint_data.py create mode 100644 src/xhshow/generators/__init__.py create mode 100644 src/xhshow/generators/fingerprint.py create mode 100644 src/xhshow/generators/fingerprint_helpers.py create mode 100644 tests/test_cookie_parsing.py diff --git a/README.md b/README.md index 4b2b513..8defa5c 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,32 @@ import requests client = Xhshow() +# 准备 cookies(支持字典或字符串格式) +cookies = { + "a1": "your_a1_value", + "web_session": "your_web_session", + "webId": "your_web_id" +} +# 或使用 Cookie 字符串格式: +# cookies = "a1=your_a1_value; web_session=your_web_session; webId=your_web_id" + # 注意: uri 参数可以传递完整 URL 或 URI 路径,会自动提取 URI headers = client.sign_headers_get( uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", # 完整 URL(推荐) # uri="/api/sns/web/v1/user_posted", # 或者只传 URI 路径 - a1_value="your_a1_cookie_value", + cookies=cookies, # 传入完整 cookies params={"num": "30", "cursor": "", "user_id": "123"} ) +# 返回的 headers 包含以下字段: +# { +# "x-s": "XYS_...", +# "x-s-common": "...", +# "x-t": "1234567890", +# "x-b3-traceid": "...", +# "x-xray-traceid": "..." +# } + # 方式1: 使用 update 方法更新现有 headers(推荐) base_headers = { "User-Agent": "Mozilla/5.0...", @@ -49,7 +67,7 @@ response = requests.get( "https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", params={"num": "30", "cursor": "", "user_id": "123"}, headers=base_headers, - cookies={"a1": "your_a1_cookie_value"} + cookies=cookies ) # 方式2: 使用 ** 解包创建新 headers @@ -61,13 +79,13 @@ response = requests.get( "Content-Type": "application/json", **headers # 解包签名 headers(会创建新字典) }, - cookies={"a1": "your_a1_cookie_value"} + cookies=cookies ) # POST 请求示例:使用 sign_headers_post headers_post = client.sign_headers_post( uri="https://edith.xiaohongshu.com/api/sns/web/v1/login", - a1_value="your_a1_cookie_value", + cookies=cookies, payload={"username": "test", "password": "123456"} ) @@ -75,7 +93,7 @@ response = requests.post( "https://edith.xiaohongshu.com/api/sns/web/v1/login", json={"username": "test", "password": "123456"}, headers={**base_headers, **headers_post}, - cookies={"a1": "your_a1_cookie_value"} + cookies=cookies ) # 构建符合xhs平台的GET请求链接 @@ -101,26 +119,30 @@ import requests client = Xhshow() -# 也可以使用统一方法 sign_headers(需要手动指定 method) +# 注意: sign_headers_* 系列方法使用 cookies 参数 +# 而 sign_xs_* 系列方法使用 a1_value 参数 + +# 使用统一方法 sign_headers(需要手动指定 method) +cookies = {"a1": "your_a1_value", "web_session": "..."} headers = client.sign_headers( method="GET", # 或 "POST" uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", - a1_value="your_a1_cookie_value", + cookies=cookies, # sign_headers 使用 cookies 参数 params={"num": "30"} # GET 请求使用 params,POST 请求使用 payload ) -# GET 请求签名 +# GET 请求签名(使用 sign_xs_get - 只需要 a1_value) x_s = client.sign_xs_get( uri="https://edith.xiaohongshu.com/api/sns/web/v1/user_posted", # v0.1.3及后续版本支持自动提取uri # uri="/api/sns/web/v1/user_posted" # v0.1.2及以前版本需要主动提取uri - a1_value="your_a1_cookie_value", + a1_value="your_a1_cookie_value", # sign_xs_* 系列使用 a1_value params={"num": "30", "cursor": "", "user_id": "123"} ) -# POST 请求签名 +# POST 请求签名(使用 sign_xs_post - 只需要 a1_value) x_s = client.sign_xs_post( uri="https://edith.xiaohongshu.com/api/sns/web/v1/login", - a1_value="your_a1_cookie_value", + a1_value="your_a1_cookie_value", # sign_xs_* 系列使用 a1_value payload={"username": "test", "password": "123456"} ) @@ -149,6 +171,30 @@ x_s = client.sign_xs_get( ) x_t = client.get_x_t(timestamp=timestamp) x_xray_traceid = client.get_xray_trace_id(timestamp=int(timestamp * 1000)) + +# 生成 x-s-common 签名 +# x-s-common 签名需要完整的 cookies +cookies = { + "a1": "your_a1_value", + "web_session": "your_web_session", + "webId": "your_web_id" +} + +# 方式1: 使用 sign_xsc 别名方法(推荐) +xs_common = client.sign_xsc(cookie_dict=cookies) + +# 方式2: 使用完整方法名 +xs_common = client.sign_xs_common(cookie_dict=cookies) + +# 方式3: 支持 Cookie 字符串格式 +cookie_string = "a1=your_a1_value; web_session=your_web_session; webId=your_web_id" +xs_common = client.sign_xsc(cookie_dict=cookie_string) + +# 使用在请求中 +headers = { + "x-s-common": xs_common, + # ... 其他 headers +} ``` @@ -180,10 +226,25 @@ client = Xhshow(config=custom_config) ## 参数说明 -- `uri`: 请求URI(去除https域名和查询参数) -- `a1_value`: cookie中的a1值 +### **sign_headers** 系列方法(推荐使用) +- `uri`: 请求 URI 或完整 URL(会自动提取 URI) +- `cookies`: 完整的 cookie 字典或 cookie 字符串 + - 字典格式: `{"a1": "...", "web_session": "...", "webId": "..."}` + - 字符串格式: `"a1=...; web_session=...; webId=..."` - `xsec_appid`: 应用标识符,默认为 `xhs-pc-web` -- `params/payload`: 请求参数(GET用params,POST用payload) +- `params`: GET 请求参数(仅在 method="GET" 时使用) +- `payload`: POST 请求参数(仅在 method="POST" 时使用) +- `timestamp`: 可选的统一时间戳(秒) + +### **sign_xs** 系列方法 +- `uri`: 请求 URI 或完整 URL +- `a1_value`: cookie 中的 a1 值(字符串) +- `xsec_appid`: 应用标识符,默认为 `xhs-pc-web` +- `params/payload`: 请求参数(GET 用 params,POST 用 payload) +- `timestamp`: 可选的统一时间戳(秒) + +### **sign_xsc** 系列方法 +- `cookie_dict`: 完整的 cookie 字典或 cookie 字符串 ## 开发环境 diff --git a/pyproject.toml b/pyproject.toml index f33adce..c084b7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,10 @@ classifiers = [ "Topic :: Security :: Cryptography", "Typing :: Typed", ] -dependencies = [] +dependencies = [ + "curl-cffi>=0.13.0", + "pycryptodome>=3.23.0", +] [project.optional-dependencies] dev = [ diff --git a/src/xhshow/client.py b/src/xhshow/client.py index 977a4bb..d71ede2 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -4,6 +4,7 @@ from typing import Any, Literal from .config import CryptoConfig +from .core.common_sign import XsCommonSigner from .core.crypto import CryptoProcessor from .utils.random_gen import RandomGenerator from .utils.url_utils import build_url, extract_uri @@ -11,6 +12,7 @@ validate_get_signature_params, validate_post_signature_params, validate_signature_params, + validate_xs_common_params, ) __all__ = ["Xhshow"] @@ -140,6 +142,23 @@ def sign_xs( json.dumps(signature_data, separators=(",", ":"), ensure_ascii=False) ) + def sign_xs_common( + self, + cookie_dict: dict[str, Any] | str, + ) -> str: + """ + Generate x-s-common signature + + Args: + cookie_dict: Complete cookie dictionary or cookie string + + Returns: + Encoded x-s-common signature string + """ + parsed_cookies = self._parse_cookies(cookie_dict) + signer = XsCommonSigner(self.config) + return signer.sign(parsed_cookies) + @validate_get_signature_params def sign_xs_get( self, @@ -200,6 +219,26 @@ def sign_xs_post( """ return self.sign_xs("POST", uri, a1_value, xsec_appid, payload=payload, timestamp=timestamp) + @validate_xs_common_params + def sign_xsc( + self, + cookie_dict: dict[str, Any] | str, + ) -> str: + """ + Convenience wrapper to generate the `x-s-common` signature. + + Args: + cookie_dict: Enter your complete cookie dictionary + + Returns: + Encoded signature string suitable for the `x-s-common` header. + + Raises: + TypeError: Parameter type error + ValueError: Parameter value error + """ + return self.sign_xs_common(cookie_dict) + def decode_x3(self, x3_signature: str) -> bytearray: """ Decrypt x3 signature (Base64 format) @@ -333,11 +372,29 @@ def get_x_t(self, timestamp: float | None = None) -> int: timestamp = time.time() return int(timestamp * 1000) + def _parse_cookies(self, cookies: dict[str, Any] | str) -> dict[str, Any]: + """ + Parse cookies to dict format + + Args: + cookies: Cookie dictionary or cookie string + + Returns: + dict: Parsed cookie dictionary + """ + if isinstance(cookies, str): + from http.cookies import SimpleCookie + + ck = SimpleCookie() + ck.load(cookies) + return {k: morsel.value for k, morsel in ck.items()} + return cookies + def sign_headers( self, method: Literal["GET", "POST"], uri: str, - a1_value: str, + cookies: dict[str, Any] | str, xsec_appid: str = "xhs-pc-web", params: dict[str, Any] | None = None, payload: dict[str, Any] | None = None, @@ -349,33 +406,34 @@ def sign_headers( Args: method: Request method ("GET" or "POST") uri: Request URI or full URL - a1_value: a1 value from cookies + cookies: Complete cookie dictionary or cookie string xsec_appid: Application identifier, defaults to `xhs-pc-web` params: GET request parameters (only used when method="GET") payload: POST request body data (only used when method="POST") timestamp: Unix timestamp in seconds (defaults to current time) Returns: - dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + dict: Complete headers including x-s, x-s-common, x-t, x-b3-traceid, x-xray-traceid Examples: >>> client = Xhshow() + >>> cookies = {"a1": "your_a1_value", "web_session": "..."} >>> # GET request >>> headers = client.sign_headers( ... method="GET", ... uri="/api/sns/web/v1/user_posted", - ... a1_value="your_a1_value", + ... cookies=cookies, ... params={"num": "30"} ... ) >>> # POST request >>> headers = client.sign_headers( ... method="POST", ... uri="/api/sns/web/v1/login", - ... a1_value="your_a1_value", + ... cookies=cookies, ... payload={"username": "test"} ... ) >>> headers.keys() - dict_keys(['x-s', 'x-t', 'x-b3-traceid', 'x-xray-traceid']) + dict_keys(['x-s', 'x-s-common', 'x-t', 'x-b3-traceid', 'x-xray-traceid']) """ if timestamp is None: timestamp = time.time() @@ -394,13 +452,21 @@ def sign_headers( else: raise ValueError(f"Unsupported method: {method}") + cookie_dict = self._parse_cookies(cookies) + + a1_value = cookie_dict.get("a1") + if not a1_value: + raise ValueError("Missing 'a1' in cookies") + x_s = self.sign_xs(method_upper, uri, a1_value, xsec_appid, request_data, timestamp) + x_s_common = self.sign_xs_common(cookie_dict) x_t = self.get_x_t(timestamp) x_b3_traceid = self.get_b3_trace_id() x_xray_traceid = self.get_xray_trace_id(timestamp=int(timestamp * 1000)) return { - "x-s": x_s, + # "x-s": x_s, + "x-s-common": x_s_common, "x-t": str(x_t), "x-b3-traceid": x_b3_traceid, "x-xray-traceid": x_xray_traceid, @@ -409,7 +475,7 @@ def sign_headers( def sign_headers_get( self, uri: str, - a1_value: str, + cookies: dict[str, Any] | str, xsec_appid: str = "xhs-pc-web", params: dict[str, Any] | None = None, timestamp: float | None = None, @@ -419,28 +485,29 @@ def sign_headers_get( Args: uri: Request URI or full URL - a1_value: a1 value from cookies + cookies: Complete cookie dictionary or cookie string xsec_appid: Application identifier, defaults to `xhs-pc-web` params: GET request parameters timestamp: Unix timestamp in seconds (defaults to current time) Returns: - dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + dict: Complete headers including x-s, x-s-common, x-t, x-b3-traceid, x-xray-traceid Examples: >>> client = Xhshow() + >>> cookies = {"a1": "your_a1_value", "web_session": "..."} >>> headers = client.sign_headers_get( ... uri="/api/sns/web/v1/user_posted", - ... a1_value="your_a1_value", + ... cookies=cookies, ... params={"num": "30"} ... ) """ - return self.sign_headers("GET", uri, a1_value, xsec_appid, params=params, timestamp=timestamp) + return self.sign_headers("GET", uri, cookies, xsec_appid, params=params, timestamp=timestamp) def sign_headers_post( self, uri: str, - a1_value: str, + cookies: dict[str, Any] | str, xsec_appid: str = "xhs-pc-web", payload: dict[str, Any] | None = None, timestamp: float | None = None, @@ -450,20 +517,21 @@ def sign_headers_post( Args: uri: Request URI or full URL - a1_value: a1 value from cookies + cookies: Complete cookie dictionary or cookie string xsec_appid: Application identifier, defaults to `xhs-pc-web` payload: POST request body data timestamp: Unix timestamp in seconds (defaults to current time) Returns: - dict: Complete headers including x-s, x-t, x-b3-traceid, x-xray-traceid + dict: Complete headers including x-s, x-s-common, x-t, x-b3-traceid, x-xray-traceid Examples: >>> client = Xhshow() + >>> cookies = {"a1": "your_a1_value", "web_session": "..."} >>> headers = client.sign_headers_post( ... uri="/api/sns/web/v1/login", - ... a1_value="your_a1_value", + ... cookies=cookies, ... payload={"username": "test", "password": "123456"} ... ) """ - return self.sign_headers("POST", uri, a1_value, xsec_appid, payload=payload, timestamp=timestamp) + return self.sign_headers("POST", uri, cookies, xsec_appid, payload=payload, timestamp=timestamp) diff --git a/src/xhshow/config/config.py b/src/xhshow/config/config.py index 6fcf380..ded396e 100644 --- a/src/xhshow/config/config.py +++ b/src/xhshow/config/config.py @@ -8,6 +8,14 @@ class CryptoConfig: """Configuration constants for cryptographic operations""" + # Gid encrypt parameters + DES_KEY = "zbp30y86" + GID_URL = "https://as.xiaohongshu.com/api/sec/v1/shield/webprofile" + DATA_PALTFORM = "Windows" + DATA_SVN = "2" + DATA_SDK_VERSION = "4.2.6" + DATA_webBuild = "5.0.3" + # Bitwise operation constants MAX_32BIT: int = 0xFFFFFFFF MAX_SIGNED_32BIT: int = 0x7FFFFFFF @@ -59,7 +67,7 @@ class CryptoConfig: ) # Prefix constants - X3_PREFIX: str = "mns0301_" + X3_PREFIX: str = "mns0101_" XYS_PREFIX: str = "XYS_" # Trace ID generation constants @@ -70,6 +78,33 @@ class CryptoConfig: XRAY_TRACE_ID_PART2_LENGTH: int = 16 B3_TRACE_ID_LENGTH: int = 16 + # b1 secret key + B1_SECRET_KEY: str = "xhswebmplfbt" + + SIGNATURE_XSCOMMON_TEMPLATE: dict[str, Any] = field( + default_factory=lambda: { + "s0": 5, + "s1": "", + "x0": "1", + "x1": "4.2.6", + "x2": "Windows", + "x3": "xhs-pc-web", + "x4": "4.86.0", + "x5": "", + "x6": "", + "x7": "", + "x8": "", + "x9": -596800761, + "x10": 0, + "x11": "normal", + } + ) + + PUBLIC_USERAGENT: str = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0" + ) + def with_overrides(self, **kwargs: Any) -> "CryptoConfig": """ Create a new config instance with overridden values diff --git a/src/xhshow/core/common_sign.py b/src/xhshow/core/common_sign.py new file mode 100644 index 0000000..2787a09 --- /dev/null +++ b/src/xhshow/core/common_sign.py @@ -0,0 +1,49 @@ +"""x-s-common signature generation""" + +import json +from typing import Any + +from ..config import CryptoConfig +from ..core.crc32_encrypt import CRC32 +from ..generators.fingerprint import FingerprintGenerator +from ..utils.encoder import Base64Encoder + +__all__ = ["XsCommonSigner"] + + +class XsCommonSigner: + """Generate x-s-common signatures""" + + def __init__(self, config: CryptoConfig | None = None): + self.config = config or CryptoConfig() + self._fp_generator = FingerprintGenerator(self.config) + self._encoder = Base64Encoder(self.config) + + def sign(self, cookie_dict: dict[str, Any]) -> str: + """ + Generate x-s-common signature + + Args: + cookie_dict: Cookie dictionary (must be dict, not string) + + Returns: + x-s-common signature string + + Raises: + KeyError: If 'a1' cookie is missing + """ + a1_value = cookie_dict["a1"] + fingerprint = self._fp_generator.generate(cookies=cookie_dict, user_agent=self.config.PUBLIC_USERAGENT) + b1 = self._fp_generator.generate_b1(fingerprint) + + x9 = CRC32.crc32_js_int(b1) + + sign_struct = dict(self.config.SIGNATURE_XSCOMMON_TEMPLATE) + sign_struct["x5"] = a1_value + sign_struct["x8"] = b1 + sign_struct["x9"] = x9 + + sign_json = json.dumps(sign_struct, separators=(",", ":"), ensure_ascii=False) + xs_common = self._encoder.encode(sign_json) + + return xs_common diff --git a/src/xhshow/core/crc32_encrypt.py b/src/xhshow/core/crc32_encrypt.py new file mode 100644 index 0000000..e74650f --- /dev/null +++ b/src/xhshow/core/crc32_encrypt.py @@ -0,0 +1,124 @@ +""" +Custom CRC32 helper wrapped in a class. + +Main entry: + CRC32.crc32_js_int(data) + +This implements a JavaScript-style CRC32 variant compatible with: + + (-1 ^ c ^ 0xEDB88320) >>> 0 + +where `c` is the intermediate CRC state produced by the core CRC32 loop. +""" + +from __future__ import annotations + +from collections.abc import Iterable + +DataLike = str | bytes | bytearray | memoryview | Iterable[int] +__all__ = ["CRC32"] + + +class CRC32: + """CRC32 calculator with a JS-compatible static entry.""" + + MASK32: int = 0xFFFFFFFF + POLY: int = 0xEDB88320 + _TABLE: list[int] | None = None + + @classmethod + def _ensure_table(cls) -> None: + """Lazy-init CRC32 lookup table once.""" + if cls._TABLE is not None: + return + + tbl = [0] * 256 + for d in range(256): + r = d + for _ in range(8): + r = ((r >> 1) ^ cls.POLY) if (r & 1) else (r >> 1) + r &= cls.MASK32 + tbl[d] = r + cls._TABLE = tbl + + @classmethod + def _crc32_core(cls, data: DataLike, *, string_mode: str = "js") -> int: + """ + Core CRC32 state update (no final NOT/XOR). + + Args: + data: + - str: interpreted depending on ``string_mode``: + * "js": lower 8 bits of ord(char) (JS charCodeAt behavior) + * "utf8": UTF-8 encode string to bytes first + - bytes / bytearray / memoryview: used as-is + - Iterable[int]: each value will be ``& 0xFF`` and treated as a byte + string_mode: How to handle string input ("js" or "utf8"). + + Returns: + Intermediate CRC state `c` (before final bitwise NOT / XOR). + """ + cls._ensure_table() + assert cls._TABLE is not None # for type checkers + + c = cls.MASK32 + + if isinstance(data, bytes | bytearray | memoryview): + it = bytes(data) + elif isinstance(data, str): + if string_mode.lower() == "utf8": + it = data.encode("utf-8") + else: # "js" mode: charCodeAt & 0xFF + it = (ord(ch) & 0xFF for ch in data) + else: + it = ((int(b) & 0xFF) for b in data) + + for b in it: + c = (cls._TABLE[((c & 0xFF) ^ b) & 0xFF] ^ (c >> 8)) & cls.MASK32 + + return c + + @staticmethod + def _to_signed32(u: int) -> int: + """ + Convert an unsigned 32-bit int to signed 32-bit representation. + + Args: + u: Unsigned 32-bit integer (0..0xFFFFFFFF). + + Returns: + Signed 32-bit integer in range [-2^31, 2^31-1]. + """ + return u - 0x100000000 if (u & 0x80000000) else u + + @classmethod + def crc32_js_int( + cls, + data: DataLike, + *, + string_mode: str = "js", + signed: bool = True, + ) -> int: + """ + JavaScript-style CRC32 public entry. + + This matches the JS expression: + + (-1 ^ c ^ 0xEDB88320) >>> 0 + + where `c` is the intermediate CRC state from `_crc32_core`. + + Args: + data: Input data (str/bytes/iterable of ints). + string_mode: How to treat string input ("js" or "utf8"). + signed: + If True, return signed 32-bit integer; + If False, return unsigned 32-bit integer (0..0xFFFFFFFF). + + Returns: + CRC32 value as 32-bit integer (signed or unsigned). + """ + c = cls._crc32_core(data, string_mode=string_mode) + a = cls.POLY + u = ((cls.MASK32 ^ c) ^ a) & cls.MASK32 # (-1 ^ c ^ a) >>> 0 + return cls._to_signed32(u) if signed else u diff --git a/src/xhshow/data/__init__.py b/src/xhshow/data/__init__.py new file mode 100644 index 0000000..9dd696c --- /dev/null +++ b/src/xhshow/data/__init__.py @@ -0,0 +1,25 @@ +"""Data constants module""" + +from .fingerprint_data import ( + BROWSER_PLUGINS, + CANVAS_HASH, + COLOR_DEPTH_OPTIONS, + CORE_OPTIONS, + DEVICE_MEMORY_OPTIONS, + FONTS, + GPU_VENDORS, + SCREEN_RESOLUTIONS, + VOICE_HASH_OPTIONS, +) + +__all__ = [ + "GPU_VENDORS", + "SCREEN_RESOLUTIONS", + "COLOR_DEPTH_OPTIONS", + "DEVICE_MEMORY_OPTIONS", + "CORE_OPTIONS", + "BROWSER_PLUGINS", + "CANVAS_HASH", + "VOICE_HASH_OPTIONS", + "FONTS", +] diff --git a/src/xhshow/data/fingerprint_data.py b/src/xhshow/data/fingerprint_data.py new file mode 100644 index 0000000..a3197ed --- /dev/null +++ b/src/xhshow/data/fingerprint_data.py @@ -0,0 +1,133 @@ +"""Browser fingerprint data constants""" + +from typing import Final + +__all__ = [ + "GPU_VENDORS", + "SCREEN_RESOLUTIONS", + "COLOR_DEPTH_OPTIONS", + "DEVICE_MEMORY_OPTIONS", + "CORE_OPTIONS", + "BROWSER_PLUGINS", + "CANVAS_HASH", + "VOICE_HASH_OPTIONS", + "FONTS", +] + +GPU_VENDORS: Final[list[str]] = [ + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 400 (0x00000166) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 4400 (0x00001112) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 4600 (0x00000412) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 520 (0x1912) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 530 (0x00001912) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 550 (0x00001512) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 6000 (0x1606) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 540 (0x1912) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Graphics 550 (0x1913) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 640 (0x161C) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 600 (0x3E80) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 620 (0x00003EA0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 655 (0x00009BC8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x000046A8) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x00009A49) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(R) Xe MAX Graphics (0x00009BC0) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A370M (0x0000AF51) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A380 (0x0000AF41) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A380M (0x0000AF5E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A550 (0x0000AF42) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A770 (0x0000AF43) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel Arc A770M (0x0000AF50) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (RPL‑P GT1) (0x0000A702) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) UHD Graphics 770 (0x00004680) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) HD Graphics 4400 (0x00001122) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (ADL‑S GT1) (0x0000A0A1) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) Graphics (RKL GT1) (0x0000A9A1) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Mesa Intel(R) UHD Graphics (CML GT2) (0x00009A14) OpenGL 4.6)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics 3000 (0x00001022) Direct3D9Ex vs_3_0 ps_3_0, igdumd64.dll)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) HD Graphics Family (0x00000A16) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris Pro OpenGL Engine, OpenGL 4.1)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 645 (0x1616) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics 655 (0x161E) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 730 (0x0000A100) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Intel)|ANGLE (Intel, Intel(R) UHD Graphics 805 (0x0000B0A0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 3 Graphics (0x000015E0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 8 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Vega 11 Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Graphics (0x00001636) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 5500 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 560 (0x000067EF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 570 (0x000067DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 580 2048SP (0x00006FDF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 590 (0x000067FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6600 (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6600 XT (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6650 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6700 XT (0x000073DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6800 (0x000073BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 6900 XT (0x000073C2) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon RX 7700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Pro 5300M OpenGL Engine, OpenGL 4.1)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon Pro 5500 XT OpenGL Engine, OpenGL 4.1)", + "Google Inc. (AMD)|ANGLE (AMD, AMD Radeon R7 370 Series (0x00006811) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (AMD)|ANGLE (AMD, ATI Technologies Inc. AMD Radeon RX Vega 64 OpenGL Engine, OpenGL 4.1)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 (0x00001C81) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1050 Ti (0x00001C8C) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 6GB (0x000010DE) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1070 (0x00001B81) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce GTX 1080 (0x00001B80) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2060 SUPER (0x00001F06) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 2070 SUPER (0x00001F10) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x0000250F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Ti (0x00002489) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 (0x00002488) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Ti (0x000028A5) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 (0x00002206) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Ti (0x00002208) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 3090 (0x00002204) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 (0x00002882) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4060 Ti (0x00002803) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 (0x00002786) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Ti (0x00002857) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4080 (0x00002819) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 (0x00002684) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro RTX 5000 Ada Generation (0x000026B2) Direct3D11 vs_5_0 ps_5_0, D3D11)", # noqa: E501 + "Google Inc. (NVIDIA)|ANGLE (NVIDIA, NVIDIA Quadro P400 (0x00001CB3) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)", # noqa: E501 + "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)", + "Google Inc. (Google)|ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device), SwiftShader driver)", +] + +SCREEN_RESOLUTIONS: Final[dict[str, list]] = { + "resolutions": ["1366;768", "1600;900", "1920;1080", "2560;1440", "3840;2160", "7680;4320"], + "weights": [0.25, 0.15, 0.35, 0.15, 0.08, 0.02], +} + +COLOR_DEPTH_OPTIONS: Final[dict[str, list]] = { + "values": [16, 24, 30, 32], + "weights": [0.05, 0.6, 0.05, 0.3], +} + +DEVICE_MEMORY_OPTIONS: Final[dict[str, list]] = { + "values": [1, 2, 4, 8, 12, 16], + "weights": [0.10, 0.25, 0.4, 0.2, 0.03, 0.01], +} + +CORE_OPTIONS: Final[dict[str, list]] = { + "values": [2, 4, 6, 8, 12, 16, 24, 32], + "weights": [0.1, 0.4, 0.2, 0.15, 0.08, 0.04, 0.02, 0.01], +} + +BROWSER_PLUGINS: Final[str] = ( + "PDF Viewer,Chrome PDF Viewer,Chromium PDF Viewer,Microsoft Edge PDF Viewer,WebKit built-in PDF" +) + +CANVAS_HASH: Final[str] = "742cc32c" + +VOICE_HASH_OPTIONS: Final[str] = "10311144241322244122" + +FONTS: Final[str] = ( + 'system-ui, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", -apple-system, "Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", sans-serif, BlinkMacSystemFont, "Helvetica Neue", Arial, "PingFang SC", "PingFang TC", "PingFang HK", "Microsoft Yahei", "Microsoft JhengHei"' # noqa: E501 +) diff --git a/src/xhshow/generators/__init__.py b/src/xhshow/generators/__init__.py new file mode 100644 index 0000000..8f8e2ee --- /dev/null +++ b/src/xhshow/generators/__init__.py @@ -0,0 +1,5 @@ +"""Generators module""" + +from .fingerprint import FingerprintGenerator + +__all__ = ["FingerprintGenerator"] diff --git a/src/xhshow/generators/fingerprint.py b/src/xhshow/generators/fingerprint.py new file mode 100644 index 0000000..db2b967 --- /dev/null +++ b/src/xhshow/generators/fingerprint.py @@ -0,0 +1,217 @@ +"""Browser fingerprint generator""" + +import hashlib +import json +import random +import secrets +import time +import urllib.parse + +from Crypto.Cipher import ARC4 + +from ..config import CryptoConfig +from ..data import fingerprint_data as FPData +from ..utils import encoder +from . import fingerprint_helpers as helpers + +__all__ = ["FingerprintGenerator"] + + +class FingerprintGenerator: + """XHS Fingerprint generation function""" + + def __init__(self, config: CryptoConfig): + self.config = config + self._b1_key = self.config.B1_SECRET_KEY.encode() + self._encoder = encoder.Base64Encoder(self.config) + + def generate_b1(self, fp: dict) -> str: + """ + Generate b1 parameter from fingerprint + + Args: + fp: Fingerprint dictionary + + Returns: + Base64 encoded b1 string + """ + b1_fp = { + "x33": fp["x33"], + "x34": fp["x34"], + "x35": fp["x35"], + "x36": fp["x36"], + "x37": fp["x37"], + "x38": fp["x38"], + "x39": fp["x39"], + "x42": fp["x42"], + "x43": fp["x43"], + "x44": fp["x44"], + "x45": fp["x45"], + "x46": fp["x46"], + "x48": fp["x48"], + "x49": fp["x49"], + "x50": fp["x50"], + "x51": fp["x51"], + "x52": fp["x52"], + "x82": fp["x82"], + } + b1_json = json.dumps(b1_fp, separators=(",", ":"), ensure_ascii=False) + cipher = ARC4.new(self._b1_key) + ciphertext = cipher.encrypt(b1_json.encode("utf-8")).decode("latin1") + encoded_url = urllib.parse.quote(ciphertext, safe="!*'()~_-") + b = [] + for c in encoded_url.split("%")[1:]: + chars = list(c) + b.append(int("".join(chars[:2]), 16)) + [b.append(ord(j)) for j in chars[2:]] + + b1 = self._encoder.encode(json.dumps(b, separators=(",", ":"))) + + return b1 + + def generate(self, cookies: dict, user_agent: str) -> dict: + """ + Generate browser fingerprint + + Args: + cookies: Cookie dictionary + user_agent: User agent string + + Returns: + Complete fingerprint dictionary + """ + cookie_string = "; ".join(f"{k}={v}" for k, v in cookies.items()) + + screen_config = helpers.get_screen_config() + is_incognito_mode = helpers.weighted_random_choice(["true", "false"], [0.95, 0.05]) + vendor, renderer = helpers.get_renderer_info() + + x78_y = random.randint(2350, 2450) + fp = { + "x1": user_agent, + "x2": "false", + "x3": "zh-CN", + "x4": helpers.weighted_random_choice( + FPData.COLOR_DEPTH_OPTIONS["values"], + FPData.COLOR_DEPTH_OPTIONS["weights"], + ), + "x5": helpers.weighted_random_choice( + FPData.DEVICE_MEMORY_OPTIONS["values"], + FPData.DEVICE_MEMORY_OPTIONS["weights"], + ), + "x6": "24", + "x7": f"{vendor},{renderer}", + "x8": helpers.weighted_random_choice(FPData.CORE_OPTIONS["values"], FPData.CORE_OPTIONS["weights"]), + "x9": f"{screen_config['width']};{screen_config['height']}", + "x10": f"{screen_config['availWidth']};{screen_config['availHeight']}", + "x11": "-480", + "x12": "Asia/Shanghai", + "x13": is_incognito_mode, + "x14": is_incognito_mode, + "x15": is_incognito_mode, + "x16": "false", + "x17": "false", + "x18": "un", + "x19": "Win32", + "x20": "", + "x21": FPData.BROWSER_PLUGINS, + "x22": helpers.generate_webgl_hash(), + "x23": "false", + "x24": "false", + "x25": "false", + "x26": "false", + "x27": "false", + "x28": "0,false,false", + "x29": "4,7,8", + "x30": "swf object not loaded", + "x33": "0", + "x34": "0", + "x35": "0", + "x36": f"{random.randint(1, 20)}", + "x37": "0|0|0|0|0|0|0|0|0|1|0|0|0|0|0|0|0|0|1|0|0|0|0|0", + "x38": "0|0|1|0|1|0|0|0|0|0|1|0|1|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0", + "x39": 0, + "x40": "0", + "x41": "0", + "x42": "3.4.4", + "x43": helpers.generate_canvas_hash(), + "x44": f"{int(time.time() * 1000)}", + "x45": "__SEC_CAV__1-1-1-1-1|__SEC_WSA__|", + "x46": "false", + "x47": "1|0|0|0|0|0", + "x48": "", + "x49": "{list:[],type:}", + "x50": "", + "x51": "", + "x52": "", + "x55": "380,380,360,400,380,400,420,380,400,400,360,360,440,420", + "x56": f"{vendor}|{renderer}|{helpers.generate_webgl_hash()}|35", + "x57": cookie_string, + "x58": "180", + "x59": "2", + "x60": "63", + "x61": "1291", + "x62": "2047", + "x63": "0", + "x64": "0", + "x65": "0", + "x66": { + "referer": "", + "location": "https://www.xiaohongshu.com/explore", + "frame": 0, + }, + "x67": "1|0", + "x68": "0", + "x69": "326|1292|30", + "x70": ["location"], + "x71": "true", + "x72": "complete", + "x73": "1191", + "x74": "0|0|0", + "x75": "Google Inc.", + "x76": "true", + "x77": "1|1|1|1|1|1|1|1|1|1", + "x78": { + "x": 0, + "y": x78_y, + "left": 0, + "right": 290.828125, + "bottom": x78_y + 18, + "height": 18, + "top": x78_y, + "width": 290.828125, + "font": FPData.FONTS, + }, + "x82": "_0x17a2|_0x1954", + "x31": "124.04347527516074", + "x79": "144|599565058866", + "x53": hashlib.md5(secrets.token_bytes(32)).hexdigest(), + "x54": FPData.VOICE_HASH_OPTIONS, + "x80": "1|[object FileSystemDirectoryHandle]", + } + + return fp + + def update(self, fp: dict, cookies: dict, url: str) -> None: + """ + Update fingerprint with new cookies and URL + + Args: + fp: Fingerprint dictionary to update + cookies: Updated cookie dictionary + url: Current URL + """ + cookie_string = "; ".join(f"{k}={v}" for k, v in cookies.items()) + + fp.update( + { + "x39": 0, + "x44": f"{time.time() * 1000}", + "x57": cookie_string, + "x66": { + "referer": "https://www.xiaohongshu.com/explore", + "location": url, + "frame": 0, + }, + } + ) diff --git a/src/xhshow/generators/fingerprint_helpers.py b/src/xhshow/generators/fingerprint_helpers.py new file mode 100644 index 0000000..b8fede4 --- /dev/null +++ b/src/xhshow/generators/fingerprint_helpers.py @@ -0,0 +1,92 @@ +"""Fingerprint generation helper functions""" + +import hashlib +import random +import secrets +from typing import Any + +from ..data import fingerprint_data as FPData + +__all__ = [ + "weighted_random_choice", + "get_renderer_info", + "get_screen_config", + "generate_canvas_hash", + "generate_webgl_hash", +] + + +def weighted_random_choice(options: list, weights: list) -> Any: + """ + Random choice a value from list according to the given weights + + Args: + options: Option list + weights: Weight list mapping the option list (without normalization) + + Returns: + Randomly chosen value from options + """ + return f"{random.choices(options, weights=weights, k=1)[0]}" + + +def get_renderer_info() -> tuple[str, str]: + """ + Get random GPU renderer information + + Returns: + Tuple of (vendor, renderer) + """ + renderer_str = random.choice(FPData.GPU_VENDORS) + vendor, renderer = renderer_str.split("|") + return vendor, renderer + + +def get_screen_config() -> dict[str, Any]: + """ + Get random screen configuration with width, height, and available dimensions + + Returns: + Dictionary containing screen configuration + """ + width_str, height_str = weighted_random_choice( + FPData.SCREEN_RESOLUTIONS["resolutions"], + FPData.SCREEN_RESOLUTIONS["weights"], + ).split(";") + + width = int(width_str) + height = int(height_str) + + if random.choice([True, False]): + avail_width = width - int(weighted_random_choice([0, 30, 60, 80], [0.1, 0.4, 0.3, 0.2])) + avail_height = height + else: + avail_width = width + avail_height = height - int(weighted_random_choice([30, 60, 80, 100], [0.2, 0.5, 0.2, 0.1])) + + return { + "width": width, + "height": height, + "availWidth": avail_width, + "availHeight": avail_height, + } + + +def generate_canvas_hash() -> str: + """ + Generate canvas fingerprint hash + + Returns: + Canvas hash string + """ + return FPData.CANVAS_HASH + + +def generate_webgl_hash() -> str: + """ + Generate WebGL fingerprint hash + + Returns: + WebGL hash (MD5 hex string) + """ + return hashlib.md5(secrets.token_bytes(32)).hexdigest() diff --git a/src/xhshow/utils/encoder.py b/src/xhshow/utils/encoder.py index 8395c7e..0d105c9 100644 --- a/src/xhshow/utils/encoder.py +++ b/src/xhshow/utils/encoder.py @@ -11,6 +11,23 @@ class Base64Encoder: def __init__(self, config: CryptoConfig): self.config = config + # Cache translation tables for better performance + self._custom_encode_table = str.maketrans( + config.STANDARD_BASE64_ALPHABET, + config.CUSTOM_BASE64_ALPHABET, + ) + self._custom_decode_table = str.maketrans( + config.CUSTOM_BASE64_ALPHABET, + config.STANDARD_BASE64_ALPHABET, + ) + self._x3_encode_table = str.maketrans( + config.STANDARD_BASE64_ALPHABET, + config.X3_BASE64_ALPHABET, + ) + self._x3_decode_table = str.maketrans( + config.X3_BASE64_ALPHABET, + config.STANDARD_BASE64_ALPHABET, + ) def encode(self, data_to_encode: str) -> str: """ @@ -26,9 +43,7 @@ def encode(self, data_to_encode: str) -> str: standard_encoded_bytes = base64.b64encode(data_bytes) standard_encoded_string = standard_encoded_bytes.decode("utf-8") - translation_table = str.maketrans(self.config.STANDARD_BASE64_ALPHABET, self.config.CUSTOM_BASE64_ALPHABET) - - return standard_encoded_string.translate(translation_table) + return standard_encoded_string.translate(self._custom_encode_table) def decode(self, encoded_string: str) -> str: """ @@ -43,11 +58,8 @@ def decode(self, encoded_string: str) -> str: Raises: ValueError: Base64 decoding failed """ - reverse_translation_table = str.maketrans( - self.config.CUSTOM_BASE64_ALPHABET, self.config.STANDARD_BASE64_ALPHABET - ) + standard_encoded_string = encoded_string.translate(self._custom_decode_table) - standard_encoded_string = encoded_string.translate(reverse_translation_table) try: decoded_bytes = base64.b64decode(standard_encoded_string) except (binascii.Error, ValueError) as e: @@ -67,9 +79,8 @@ def decode_x3(self, encoded_string: str) -> bytes: Raises: ValueError: Base64 decoding failed """ - reverse_translation_table = str.maketrans(self.config.X3_BASE64_ALPHABET, self.config.STANDARD_BASE64_ALPHABET) + standard_encoded_string = encoded_string.translate(self._x3_decode_table) - standard_encoded_string = encoded_string.translate(reverse_translation_table) try: decoded_bytes = base64.b64decode(standard_encoded_string) except (binascii.Error, ValueError) as e: @@ -89,6 +100,4 @@ def encode_x3(self, input_bytes: bytes | bytearray) -> str: standard_encoded_bytes = base64.b64encode(input_bytes) standard_encoded_string = standard_encoded_bytes.decode("utf-8") - translation_table = str.maketrans(self.config.STANDARD_BASE64_ALPHABET, self.config.X3_BASE64_ALPHABET) - - return standard_encoded_string.translate(translation_table) + return standard_encoded_string.translate(self._x3_encode_table) diff --git a/src/xhshow/utils/validators.py b/src/xhshow/utils/validators.py index 82ad3d1..6570dce 100644 --- a/src/xhshow/utils/validators.py +++ b/src/xhshow/utils/validators.py @@ -12,6 +12,7 @@ "validate_signature_params", "validate_get_signature_params", "validate_post_signature_params", + "validate_xs_common_params", ] @@ -76,6 +77,18 @@ def validate_payload(payload: Any) -> dict[str, Any] | None: return payload + @staticmethod + def validate_cookie(cookie: Any) -> dict[str, Any] | str: + """Validate cookie parameter""" + if cookie is not None and not (isinstance(cookie, dict) or isinstance(cookie, str)): + raise TypeError(f"payload must be dict or None, got {type(cookie).__name__}") + # detect cookie dict validation + if cookie is not None and isinstance(cookie, dict): + for key in cookie.keys(): + if not isinstance(key, str): + raise TypeError(f"payload keys must be str, got {type(key).__name__} for key '{key}'") + return cookie + def validate_signature_params(func: F) -> F: # type: ignore # noqa: UP047 """ @@ -195,3 +208,35 @@ def wrapper( ) return wrapper # type: ignore + + +def validate_xs_common_params(func: F) -> F: # type: ignore[misc] # noqa: UP047 + """ + Parameter validation decorator for the `sign_xsc` method. + + This wrapper normalizes and validates the arguments before delegating to + the underlying signing implementation. + + Args: + func: Method to be decorated. + + Returns: + Wrapped method with validated parameters. + """ + + @wraps(func) + def wrapper( + self, + cookie_dict: dict[str, Any] | None = None, + ) -> str: + validator = RequestSignatureValidator() + + # Reuse existing validators where possible + validated_cookie_dict = validator.validate_cookie(cookie_dict) + + return func( + self, + validated_cookie_dict, + ) + + return wrapper # type: ignore diff --git a/tests/test_cookie_parsing.py b/tests/test_cookie_parsing.py new file mode 100644 index 0000000..532df01 --- /dev/null +++ b/tests/test_cookie_parsing.py @@ -0,0 +1,425 @@ +"""Tests for cookie parsing and sign_headers functionality""" + +import pytest + +from xhshow import Xhshow +from xhshow.core.common_sign import XsCommonSigner + + +class TestCookieParsing: + """测试 Cookie 解析功能""" + + def setup_method(self): + self.client = Xhshow() + + def test_parse_cookies_from_dict(self): + """测试从字典解析 cookies""" + cookies_dict = { + "a1": "test_a1_value", + "web_session": "test_session", + "webId": "test_web_id", + } + + result = self.client._parse_cookies(cookies_dict) + + assert isinstance(result, dict) + assert result == cookies_dict + assert result is cookies_dict # Should return the same object for dict input + + def test_parse_cookies_from_string(self): + """测试从字符串解析 cookies""" + cookie_string = "a1=test_a1_value; web_session=test_session; webId=test_web_id" + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert "a1" in result + assert "web_session" in result + assert "webId" in result + assert result["a1"] == "test_a1_value" + assert result["web_session"] == "test_session" + assert result["webId"] == "test_web_id" + + def test_parse_cookies_from_string_with_spaces(self): + """测试解析带空格的 cookie 字符串""" + cookie_string = "a1=value1; web_session=value2 ;webId=value3" + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert "a1" in result + assert "web_session" in result + assert "webId" in result + + def test_parse_cookies_from_string_with_special_chars(self): + """测试解析包含特殊字符的 cookie""" + cookie_string = 'a1=abc123_-.; web_session="quoted value"; key=value=' + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert "a1" in result + assert "web_session" in result + + def test_parse_cookies_empty_dict(self): + """测试空字典解析""" + result = self.client._parse_cookies({}) + + assert isinstance(result, dict) + assert len(result) == 0 + + def test_parse_cookies_empty_string(self): + """测试空字符串解析""" + result = self.client._parse_cookies("") + + assert isinstance(result, dict) + assert len(result) == 0 + + +class TestSignXsCommon: + """测试 x-s-common 签名功能""" + + def setup_method(self): + self.client = Xhshow() + + def test_sign_xs_common_with_dict(self): + """测试使用字典生成 x-s-common""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + "webId": "test_web_id", + } + + result = self.client.sign_xs_common(cookies) + + assert isinstance(result, str) + assert len(result) > 0 + + def test_sign_xs_common_with_string(self): + """测试使用字符串生成 x-s-common""" + cookie_string = "a1=test_a1_value; web_session=test_session; webId=test_web_id" + + result = self.client.sign_xs_common(cookie_string) + + assert isinstance(result, str) + assert len(result) > 0 + + def test_sign_xs_common_consistency(self): + """测试字典和字符串输入的一致性""" + cookies_dict = { + "a1": "test_a1_value", + "web_session": "test_session", + "webId": "test_web_id", + } + cookie_string = "a1=test_a1_value; web_session=test_session; webId=test_web_id" + + result_dict = self.client.sign_xs_common(cookies_dict) + result_string = self.client.sign_xs_common(cookie_string) + + # Both should produce valid results + assert isinstance(result_dict, str) + assert isinstance(result_string, str) + assert len(result_dict) > 0 + assert len(result_string) > 0 + + def test_sign_xs_common_missing_a1(self): + """测试缺少 a1 时的异常处理""" + cookies = { + "web_session": "test_session", + "webId": "test_web_id", + } + + with pytest.raises(KeyError, match="a1"): + self.client.sign_xs_common(cookies) + + def test_sign_xsc_alias(self): + """测试 sign_xsc 别名方法""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + result = self.client.sign_xsc(cookies) + + assert isinstance(result, str) + assert len(result) > 0 + + +class TestXsCommonSigner: + """测试 XsCommonSigner 类""" + + def setup_method(self): + self.signer = XsCommonSigner() + + def test_sign_with_dict_only(self): + """测试只接受字典参数""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + "webId": "test_web_id", + } + + result = self.signer.sign(cookies) + + assert isinstance(result, str) + assert len(result) > 0 + + def test_sign_missing_a1(self): + """测试缺少 a1 的异常""" + cookies = { + "web_session": "test_session", + } + + with pytest.raises(KeyError, match="a1"): + self.signer.sign(cookies) + + def test_sign_reproducibility(self): + """测试签名的可重现性""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + result1 = self.signer.sign(cookies) + result2 = self.signer.sign(cookies) + + # Note: May contain random elements, so just check format + assert isinstance(result1, str) + assert isinstance(result2, str) + assert len(result1) > 0 + assert len(result2) > 0 + + +class TestSignHeaders: + """测试 sign_headers 系列方法""" + + def setup_method(self): + self.client = Xhshow() + + def test_sign_headers_get_with_dict(self): + """测试 GET 请求使用字典 cookies""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + headers = self.client.sign_headers( + method="GET", + uri="/api/sns/web/v1/user_posted", + cookies=cookies, + params={"num": "30"}, + ) + + assert isinstance(headers, dict) + assert "x-s" in headers + assert "x-s-common" in headers + assert "x-t" in headers + assert "x-b3-traceid" in headers + assert "x-xray-traceid" in headers + + assert headers["x-s"].startswith("XYS_") + assert len(headers["x-s-common"]) > 0 + assert headers["x-t"].isdigit() + assert len(headers["x-b3-traceid"]) == 16 + assert len(headers["x-xray-traceid"]) == 32 + + def test_sign_headers_get_with_string(self): + """测试 GET 请求使用字符串 cookies""" + cookie_string = "a1=test_a1_value; web_session=test_session; webId=test_web_id" + + headers = self.client.sign_headers( + method="GET", + uri="/api/sns/web/v1/user_posted", + cookies=cookie_string, + params={"num": "30"}, + ) + + assert isinstance(headers, dict) + assert "x-s" in headers + assert "x-s-common" in headers + assert headers["x-s"].startswith("XYS_") + assert len(headers["x-s-common"]) > 0 + + def test_sign_headers_post_with_dict(self): + """测试 POST 请求使用字典 cookies""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + headers = self.client.sign_headers( + method="POST", + uri="/api/sns/web/v1/login", + cookies=cookies, + payload={"username": "test", "password": "123456"}, + ) + + assert isinstance(headers, dict) + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert headers["x-s"].startswith("XYS_") + assert len(headers["x-s-common"]) > 0 + + def test_sign_headers_post_with_string(self): + """测试 POST 请求使用字符串 cookies""" + cookie_string = "a1=test_a1_value; web_session=test_session" + + headers = self.client.sign_headers( + method="POST", + uri="/api/sns/web/v1/login", + cookies=cookie_string, + payload={"username": "test"}, + ) + + assert isinstance(headers, dict) + assert "x-s" in headers + assert "x-s-common" in headers + assert headers["x-s"].startswith("XYS_") + + def test_sign_headers_missing_a1(self): + """测试缺少 a1 cookie 的异常处理""" + cookies = { + "web_session": "test_session", + } + + with pytest.raises(ValueError, match="Missing 'a1' in cookies"): + self.client.sign_headers( + method="GET", + uri="/api/test", + cookies=cookies, + params={"key": "value"}, + ) + + def test_sign_headers_get_convenience(self): + """测试 sign_headers_get 便捷方法""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + headers = self.client.sign_headers_get( + uri="/api/sns/web/v1/user_posted", + cookies=cookies, + params={"num": "30"}, + ) + + assert isinstance(headers, dict) + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) + + def test_sign_headers_post_convenience(self): + """测试 sign_headers_post 便捷方法""" + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + + headers = self.client.sign_headers_post( + uri="/api/sns/web/v1/login", + cookies=cookies, + payload={"username": "test"}, + ) + + assert isinstance(headers, dict) + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) + + def test_sign_headers_with_timestamp(self): + """测试使用自定义时间戳""" + import time + + cookies = { + "a1": "test_a1_value", + "web_session": "test_session", + } + custom_ts = time.time() + + headers = self.client.sign_headers( + method="GET", + uri="/api/test", + cookies=cookies, + params={"key": "value"}, + timestamp=custom_ts, + ) + + assert headers["x-t"] == str(int(custom_ts * 1000)) + + def test_cookie_parsing_only_once(self): + """测试 cookie 只被解析一次(性能测试)""" + # This is more of a design verification test + # We can't directly test parse count, but we verify the result is correct + + cookie_string = "a1=test_a1_value; web_session=test_session; webId=test_web_id" + + headers = self.client.sign_headers( + method="GET", + uri="/api/test", + cookies=cookie_string, + params={"key": "value"}, + ) + + # Both x-s and x-s-common should work correctly + assert headers["x-s"].startswith("XYS_") + assert len(headers["x-s-common"]) > 0 + + # Verify all expected headers are present + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) + + +class TestCookieParsingEdgeCases: + """测试 Cookie 解析的边界情况""" + + def setup_method(self): + self.client = Xhshow() + + def test_cookie_with_equals_in_value(self): + """测试 cookie 值中包含等号""" + cookie_string = "a1=abc=def=ghi; web_session=test" + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert "a1" in result + # SimpleCookie handles this - value should be everything after first = + assert "=" in result["a1"] or result["a1"] == "abc" + + def test_cookie_with_semicolon_in_quoted_value(self): + """测试引号内包含分号的 cookie""" + cookie_string = 'a1="value;with;semicolons"; web_session=test' + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert "a1" in result + assert "web_session" in result + + def test_cookie_with_unicode(self): + """测试包含 Unicode 字符的 cookie""" + cookies_dict = { + "a1": "test_a1_值", + "web_session": "测试", + } + + result = self.client._parse_cookies(cookies_dict) + + assert result == cookies_dict + assert result["a1"] == "test_a1_值" + assert result["web_session"] == "测试" + + def test_cookie_string_only_one_cookie(self): + """测试只有一个 cookie 的字符串""" + cookie_string = "a1=test_value" + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + assert len(result) == 1 + assert result["a1"] == "test_value" + + def test_cookie_with_path_and_domain(self): + """测试带有 path 和 domain 的 cookie 字符串""" + # SimpleCookie can handle full cookie attributes + cookie_string = "a1=value1; Path=/; Domain=.example.com; web_session=value2" + + result = self.client._parse_cookies(cookie_string) + + assert isinstance(result, dict) + # Should extract cookie names and values, ignoring attributes + assert "a1" in result or "Path" in result # SimpleCookie behavior diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 70be442..5bad44c 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -1,6 +1,7 @@ import pytest from xhshow import CryptoProcessor, Xhshow +from xhshow.core.crc32_encrypt import CRC32 class TestCryptoProcessor: @@ -516,15 +517,17 @@ def test_sign_headers(self): client = Xhshow() # Test GET request headers with params + cookies = {"a1": "test_a1_value", "web_session": "test_session"} headers = client.sign_headers( method="GET", uri="/api/sns/web/v1/user_posted", - a1_value="test_a1_value", + cookies=cookies, params={"num": "30", "cursor": "", "user_id": "123"}, ) assert isinstance(headers, dict) assert "x-s" in headers + assert "x-s-common" in headers assert "x-t" in headers assert "x-b3-traceid" in headers assert "x-xray-traceid" in headers @@ -538,19 +541,19 @@ def test_sign_headers(self): headers_post = client.sign_headers( method="POST", uri="/api/sns/web/v1/login", - a1_value="test_a1_value", + cookies=cookies, payload={"username": "test", "password": "123456"}, ) assert isinstance(headers_post, dict) - assert all(k in headers_post for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert all(k in headers_post for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) # Test with custom timestamp custom_ts = time.time() headers_custom = client.sign_headers( method="GET", uri="/api/test", - a1_value="test_a1", + cookies=cookies, params={"key": "value"}, timestamp=custom_ts, ) @@ -570,21 +573,22 @@ def test_sign_headers_get(self): client = Xhshow() # Test basic usage + cookies = {"a1": "test_a1_value", "web_session": "test_session"} headers = client.sign_headers_get( uri="/api/sns/web/v1/user_posted", - a1_value="test_a1_value", + cookies=cookies, params={"num": "30", "cursor": "", "user_id": "123"}, ) assert isinstance(headers, dict) - assert all(k in headers for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) assert headers["x-s"].startswith("XYS_") assert all(isinstance(v, str) for v in headers.values()) # Test with custom timestamp custom_ts = time.time() headers_ts = client.sign_headers_get( - uri="/api/test", a1_value="test_a1", params={"key": "value"}, timestamp=custom_ts + uri="/api/test", cookies=cookies, params={"key": "value"}, timestamp=custom_ts ) assert headers_ts["x-t"] == str(int(custom_ts * 1000)) @@ -596,21 +600,22 @@ def test_sign_headers_post(self): client = Xhshow() # Test basic usage + cookies = {"a1": "test_a1_value", "web_session": "test_session"} headers = client.sign_headers_post( uri="/api/sns/web/v1/login", - a1_value="test_a1_value", + cookies=cookies, payload={"username": "test", "password": "123456"}, ) assert isinstance(headers, dict) - assert all(k in headers for k in ["x-s", "x-t", "x-b3-traceid", "x-xray-traceid"]) + assert all(k in headers for k in ["x-s", "x-s-common", "x-t", "x-b3-traceid", "x-xray-traceid"]) assert headers["x-s"].startswith("XYS_") assert all(isinstance(v, str) for v in headers.values()) # Test with custom timestamp custom_ts = time.time() headers_ts = client.sign_headers_post( - uri="/api/test", a1_value="test_a1", payload={"key": "value"}, timestamp=custom_ts + uri="/api/test", cookies=cookies, payload={"key": "value"}, timestamp=custom_ts ) assert headers_ts["x-t"] == str(int(custom_ts * 1000)) @@ -619,12 +624,14 @@ def test_sign_headers_parameter_validation(self): """测试 sign_headers 参数验证""" client = Xhshow() + cookies = {"a1": "test_a1", "web_session": "test_session"} + # Test GET request with payload should raise error with pytest.raises(ValueError, match="GET requests must use 'params', not 'payload'"): client.sign_headers( method="GET", uri="/api/test", - a1_value="test_a1", + cookies=cookies, payload={"key": "value"}, ) @@ -633,7 +640,7 @@ def test_sign_headers_parameter_validation(self): client.sign_headers( method="POST", uri="/api/test", - a1_value="test_a1", + cookies=cookies, params={"key": "value"}, ) @@ -642,6 +649,58 @@ def test_sign_headers_parameter_validation(self): client.sign_headers( method="PUT", uri="/api/test", - a1_value="test_a1", + cookies=cookies, params={"key": "value"}, ) + + +class TestCRC32: + """测试 CRC32 加密功能""" + + def test_crc32_js_int_basic(self): + """测试基本的 CRC32 计算""" + test_string = ( + "I38rHdgsjopgIvesdVwgIC+oIELmBZ5e3VwXLgFTIxS3bqwErFeexd0ekncAzMFYnqthIhJeSBMDKutRI3KsYorWHPtGrbV0P9W" + ) + result = CRC32.crc32_js_int(test_string) + + assert isinstance(result, int) + assert result == 679790455 + + def test_crc32_signed_unsigned(self): + """测试有符号和无符号结果""" + test_data = "test_data" + + signed_result = CRC32.crc32_js_int(test_data, signed=True) + unsigned_result = CRC32.crc32_js_int(test_data, signed=False) + + assert isinstance(signed_result, int) + assert isinstance(unsigned_result, int) + assert -2147483648 <= signed_result <= 2147483647 + assert 0 <= unsigned_result <= 0xFFFFFFFF + + def test_crc32_string_modes(self): + """测试不同的字符串模式""" + test_string = "测试中文" + + js_result = CRC32.crc32_js_int(test_string, string_mode="js") + utf8_result = CRC32.crc32_js_int(test_string, string_mode="utf8") + + assert isinstance(js_result, int) + assert isinstance(utf8_result, int) + # JS mode 和 UTF8 mode 对中文的处理应该不同 + assert js_result != utf8_result + + def test_crc32_bytes_input(self): + """测试字节输入""" + test_bytes = b"test_bytes" + result = CRC32.crc32_js_int(test_bytes) + + assert isinstance(result, int) + + def test_crc32_iterable_input(self): + """测试可迭代输入""" + test_list = [72, 101, 108, 108, 111] # "Hello" + result = CRC32.crc32_js_int(test_list) + + assert isinstance(result, int) diff --git a/uv.lock b/uv.lock index dc4b790..4efe10c 100644 --- a/uv.lock +++ b/uv.lock @@ -45,6 +45,97 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, ] +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "click" version = "8.2.1" @@ -66,6 +157,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "curl-cffi" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/3d/f39ca1f8fdf14408888e7c25e15eed63eac5f47926e206fb93300d28378c/curl_cffi-0.13.0.tar.gz", hash = "sha256:62ecd90a382bd5023750e3606e0aa7cb1a3a8ba41c14270b8e5e149ebf72c5ca", size = 151303, upload-time = "2025-08-06T13:05:42.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/d1/acabfd460f1de26cad882e5ef344d9adde1507034528cb6f5698a2e6a2f1/curl_cffi-0.13.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:434cadbe8df2f08b2fc2c16dff2779fb40b984af99c06aa700af898e185bb9db", size = 5686337, upload-time = "2025-08-06T13:05:28.985Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1c/cdb4fb2d16a0e9de068e0e5bc02094e105ce58a687ff30b4c6f88e25a057/curl_cffi-0.13.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:59afa877a9ae09efa04646a7d068eeea48915a95d9add0a29854e7781679fcd7", size = 2994613, upload-time = "2025-08-06T13:05:31.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3e/fdf617c1ec18c3038b77065d484d7517bb30f8fb8847224eb1f601a4e8bc/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06ed389e45a7ca97b17c275dbedd3d6524560270e675c720e93a2018a766076", size = 7931353, upload-time = "2025-08-06T13:05:32.273Z" }, + { url = "https://files.pythonhosted.org/packages/3d/10/6f30c05d251cf03ddc2b9fd19880f3cab8c193255e733444a2df03b18944/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4e0de45ab3b7a835c72bd53640c2347415111b43421b5c7a1a0b18deae2e541", size = 7486378, upload-time = "2025-08-06T13:05:33.672Z" }, + { url = "https://files.pythonhosted.org/packages/77/81/5bdb7dd0d669a817397b2e92193559bf66c3807f5848a48ad10cf02bf6c7/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb4083371bbb94e9470d782de235fb5268bf43520de020c9e5e6be8f395443f", size = 8328585, upload-time = "2025-08-06T13:05:35.28Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c1/df5c6b4cfad41c08442e0f727e449f4fb5a05f8aa564d1acac29062e9e8e/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:28911b526e8cd4aa0e5e38401bfe6887e8093907272f1f67ca22e6beb2933a51", size = 8739831, upload-time = "2025-08-06T13:05:37.078Z" }, + { url = "https://files.pythonhosted.org/packages/1a/91/6dd1910a212f2e8eafe57877bcf97748eb24849e1511a266687546066b8a/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d433ffcb455ab01dd0d7bde47109083aa38b59863aa183d29c668ae4c96bf8e", size = 8711908, upload-time = "2025-08-06T13:05:38.741Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e4/15a253f9b4bf8d008c31e176c162d2704a7e0c5e24d35942f759df107b68/curl_cffi-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:66a6b75ce971de9af64f1b6812e275f60b88880577bac47ef1fa19694fa21cd3", size = 1614510, upload-time = "2025-08-06T13:05:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0f/9c5275f17ad6ff5be70edb8e0120fdc184a658c9577ca426d4230f654beb/curl_cffi-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:d438a3b45244e874794bc4081dc1e356d2bb926dcc7021e5a8fef2e2105ef1d8", size = 1365753, upload-time = "2025-08-06T13:05:41.879Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -132,6 +244,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, + { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -248,6 +404,10 @@ wheels = [ [[package]] name = "xhshow" source = { editable = "." } +dependencies = [ + { name = "curl-cffi" }, + { name = "pycryptodome" }, +] [package.optional-dependencies] dev = [ @@ -267,6 +427,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, + { name = "curl-cffi", specifier = ">=0.13.0" }, + { name = "pycryptodome", specifier = ">=3.23.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, From 6c9853da3b240e03742ec826a0cd726a87977bc8 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Sun, 7 Dec 2025 02:09:56 +0800 Subject: [PATCH 10/12] chore(deps): remove unused curl-cffi dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c084b7b..6495970 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,6 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "curl-cffi>=0.13.0", "pycryptodome>=3.23.0", ] From 2376168e791fe387e2a499082162012542d3cbbd Mon Sep 17 00:00:00 2001 From: Cloxl Date: Sun, 7 Dec 2025 02:16:22 +0800 Subject: [PATCH 11/12] fix: uncomment x-s signature code --- src/xhshow/client.py | 2 +- uv.lock | 123 ------------------------------------------- 2 files changed, 1 insertion(+), 124 deletions(-) diff --git a/src/xhshow/client.py b/src/xhshow/client.py index d71ede2..139881f 100644 --- a/src/xhshow/client.py +++ b/src/xhshow/client.py @@ -465,7 +465,7 @@ def sign_headers( x_xray_traceid = self.get_xray_trace_id(timestamp=int(timestamp * 1000)) return { - # "x-s": x_s, + "x-s": x_s, "x-s-common": x_s_common, "x-t": str(x_t), "x-b3-traceid": x_b3_traceid, diff --git a/uv.lock b/uv.lock index 4efe10c..4bf4396 100644 --- a/uv.lock +++ b/uv.lock @@ -45,97 +45,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, ] -[[package]] -name = "certifi" -version = "2025.11.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - [[package]] name = "click" version = "8.2.1" @@ -157,27 +66,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "curl-cffi" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4e/3d/f39ca1f8fdf14408888e7c25e15eed63eac5f47926e206fb93300d28378c/curl_cffi-0.13.0.tar.gz", hash = "sha256:62ecd90a382bd5023750e3606e0aa7cb1a3a8ba41c14270b8e5e149ebf72c5ca", size = 151303, upload-time = "2025-08-06T13:05:42.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/d1/acabfd460f1de26cad882e5ef344d9adde1507034528cb6f5698a2e6a2f1/curl_cffi-0.13.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:434cadbe8df2f08b2fc2c16dff2779fb40b984af99c06aa700af898e185bb9db", size = 5686337, upload-time = "2025-08-06T13:05:28.985Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1c/cdb4fb2d16a0e9de068e0e5bc02094e105ce58a687ff30b4c6f88e25a057/curl_cffi-0.13.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:59afa877a9ae09efa04646a7d068eeea48915a95d9add0a29854e7781679fcd7", size = 2994613, upload-time = "2025-08-06T13:05:31.027Z" }, - { url = "https://files.pythonhosted.org/packages/04/3e/fdf617c1ec18c3038b77065d484d7517bb30f8fb8847224eb1f601a4e8bc/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06ed389e45a7ca97b17c275dbedd3d6524560270e675c720e93a2018a766076", size = 7931353, upload-time = "2025-08-06T13:05:32.273Z" }, - { url = "https://files.pythonhosted.org/packages/3d/10/6f30c05d251cf03ddc2b9fd19880f3cab8c193255e733444a2df03b18944/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4e0de45ab3b7a835c72bd53640c2347415111b43421b5c7a1a0b18deae2e541", size = 7486378, upload-time = "2025-08-06T13:05:33.672Z" }, - { url = "https://files.pythonhosted.org/packages/77/81/5bdb7dd0d669a817397b2e92193559bf66c3807f5848a48ad10cf02bf6c7/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb4083371bbb94e9470d782de235fb5268bf43520de020c9e5e6be8f395443f", size = 8328585, upload-time = "2025-08-06T13:05:35.28Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c1/df5c6b4cfad41c08442e0f727e449f4fb5a05f8aa564d1acac29062e9e8e/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:28911b526e8cd4aa0e5e38401bfe6887e8093907272f1f67ca22e6beb2933a51", size = 8739831, upload-time = "2025-08-06T13:05:37.078Z" }, - { url = "https://files.pythonhosted.org/packages/1a/91/6dd1910a212f2e8eafe57877bcf97748eb24849e1511a266687546066b8a/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d433ffcb455ab01dd0d7bde47109083aa38b59863aa183d29c668ae4c96bf8e", size = 8711908, upload-time = "2025-08-06T13:05:38.741Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e4/15a253f9b4bf8d008c31e176c162d2704a7e0c5e24d35942f759df107b68/curl_cffi-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:66a6b75ce971de9af64f1b6812e275f60b88880577bac47ef1fa19694fa21cd3", size = 1614510, upload-time = "2025-08-06T13:05:40.451Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0f/9c5275f17ad6ff5be70edb8e0120fdc184a658c9577ca426d4230f654beb/curl_cffi-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:d438a3b45244e874794bc4081dc1e356d2bb926dcc7021e5a8fef2e2105ef1d8", size = 1365753, upload-time = "2025-08-06T13:05:41.879Z" }, -] - [[package]] name = "exceptiongroup" version = "1.3.0" @@ -244,15 +132,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "pycparser" -version = "2.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, -] - [[package]] name = "pycryptodome" version = "3.23.0" @@ -405,7 +284,6 @@ wheels = [ name = "xhshow" source = { editable = "." } dependencies = [ - { name = "curl-cffi" }, { name = "pycryptodome" }, ] @@ -427,7 +305,6 @@ dev = [ [package.metadata] requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, - { name = "curl-cffi", specifier = ">=0.13.0" }, { name = "pycryptodome", specifier = ">=3.23.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, From ac42f1e5474815e378d0161bb59d7b88a1c452f7 Mon Sep 17 00:00:00 2001 From: Cloxl Date: Sun, 7 Dec 2025 03:12:15 +0800 Subject: [PATCH 12/12] fix(config): update X3_PREFIX from mns0101_ to mns0301_ --- src/xhshow/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xhshow/config/config.py b/src/xhshow/config/config.py index ded396e..0ec2ec0 100644 --- a/src/xhshow/config/config.py +++ b/src/xhshow/config/config.py @@ -67,7 +67,7 @@ class CryptoConfig: ) # Prefix constants - X3_PREFIX: str = "mns0101_" + X3_PREFIX: str = "mns0301_" XYS_PREFIX: str = "XYS_" # Trace ID generation constants