From 6c768b6e7b019c91ca42aeaae014929540295c3f Mon Sep 17 00:00:00 2001 From: danial Date: Mon, 3 Nov 2025 19:35:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(jd):=20=E6=B7=BB=E5=8A=A0=E4=BA=AC?= =?UTF-8?q?=E4=B8=9C=E7=9B=B8=E5=85=B3=E8=B7=AF=E7=94=B1=E5=8F=8A=E8=8B=B9?= =?UTF-8?q?=E6=9E=9C=E6=9D=83=E7=9B=8A=E5=85=85=E5=80=BC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增jd模块基础路由,整合app_store和payment子路由 - 实现苹果权益充值接口,支持苹果、携程及沃尔玛多个渠道 - 实现卡号密码查询接口,支持不同类别订单查询 - 新增短信认证相关接口,实现短信验证码发送及短信登录 - 新增商品管理接口,支持SKU详情查询及账号类下单功能 - 新增订单管理接口,实现订单删除功能 - 实现支付相关接口,增加刷新支付参数功能 - 定义完整请求及响应数据模型,确保接口数据规范 - 编写AppStoreSpider类,封装苹果应用内订单处理逻辑 - 引入多种代理池及请求重试机制,增强接口稳定性 - 添加详细日志记录,便于请求追踪与错误排查 --- apps/jd/router/__init__.py | 11 + apps/jd/router/app_store.py | 216 +++++++ apps/jd/router/auth.py | 41 ++ apps/jd/router/goods.py | 65 +++ apps/jd/router/order.py | 25 + apps/jd/router/payment.py | 96 ++++ apps/jd/schemas/models.py | 110 ++++ apps/jd/services/app_store.py | 773 +++++++++++++++++++++++++ apps/jd/services/ctrip.py | 817 +++++++++++++++++++++++++++ apps/jd/services/delete.py | 237 ++++++++ apps/jd/services/game_area.py | 44 ++ apps/jd/services/goods_apple_card.py | 145 +++++ apps/jd/services/jstk.py | 690 ++++++++++++++++++++++ apps/jd/services/login.py | 329 +++++++++++ apps/jd/services/utils.py | 193 +++++++ apps/shared/proxy_pool/__init__.py | 0 apps/shared/proxy_pool/proxy_pool.py | 442 +++++++++++++++ core/config.py | 106 ++-- core/exceptions.py | 145 +++-- core/responses.py | 171 +++--- main.py | 59 +- pyproject.toml | 6 + uv.lock | 398 +++++++++++++ 23 files changed, 4892 insertions(+), 227 deletions(-) create mode 100644 apps/jd/router/__init__.py create mode 100644 apps/jd/router/app_store.py create mode 100644 apps/jd/router/auth.py create mode 100644 apps/jd/router/goods.py create mode 100644 apps/jd/router/order.py create mode 100644 apps/jd/router/payment.py create mode 100644 apps/jd/schemas/models.py create mode 100644 apps/jd/services/app_store.py create mode 100644 apps/jd/services/ctrip.py create mode 100644 apps/jd/services/delete.py create mode 100644 apps/jd/services/game_area.py create mode 100644 apps/jd/services/goods_apple_card.py create mode 100644 apps/jd/services/jstk.py create mode 100644 apps/jd/services/login.py create mode 100644 apps/jd/services/utils.py create mode 100644 apps/shared/proxy_pool/__init__.py create mode 100644 apps/shared/proxy_pool/proxy_pool.py diff --git a/apps/jd/router/__init__.py b/apps/jd/router/__init__.py new file mode 100644 index 0000000..6385457 --- /dev/null +++ b/apps/jd/router/__init__.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter + + +def router() -> APIRouter: + router = APIRouter(prefix="/jd-anti-risk-spider") + + from apps.jd.router import app_store, payment + + router.include_router(app_store.router) + router.include_router(payment.router) + return router diff --git a/apps/jd/router/app_store.py b/apps/jd/router/app_store.py new file mode 100644 index 0000000..8db3d3d --- /dev/null +++ b/apps/jd/router/app_store.py @@ -0,0 +1,216 @@ +from typing import Any + + +import traceback + +from fastapi import APIRouter +from fastapi.concurrency import run_in_threadpool + +from apps.jd.schemas.models import ( + AppStoreRequest, + AppleStoreRequestCategoryEnum, + PlatPayResponseData, + QueryCardRequest, + QueryCardResponseData, +) +from apps.jd.services.app_store import AppStoreSpider +from apps.jd.services.ctrip import XiechengCardSpider +from apps.shared.proxy_pool.proxy_pool import ProxyPoolFactory +from core.config import ProxyPoolType +from core.responses import ApiResponse, BusinessCode, error, success +from observability.logging import LoggerAdapter, get_logger_with_trace + +router = APIRouter(prefix="/jd", tags=["苹果权益充值"]) +logger: LoggerAdapter = get_logger_with_trace(__name__) + + +@router.post("/app/store", response_model=ApiResponse[PlatPayResponseData]) +async def app_store( + request_data: AppStoreRequest, +) -> ApiResponse[PlatPayResponseData] | ApiResponse[Any]: + """苹果权益充值""" + # 接收参数 + order_num = request_data.order_num + cookies = request_data.cookies.strip() + res = {} + + try: + match request_data.category: + case AppleStoreRequestCategoryEnum.Apple: + return error(code=BusinessCode.NOT_IMPLEMENTED) + # app_store_ = AppStoreSpider( + # cookies=cookies, + # face_price=request_data.face_price, + # order_num=order_num, + # ) + # code, res, msg = await run_in_threadpool(app_store_.run) + # logger.info(msg=f"订单ID:{order_num},最终返回日志:{res}") + # return success(data=res, message=msg) + case AppleStoreRequestCategoryEnum.CTrip: + # 100:10164405229636 + # 200:10164405699325 + # 300:10164405273963 + # 500:10164404344795 + # 200:10155862962901 + # 300:10155867355510 + # 500:10155863307550 + # 1000:10155867606890 + # 1000:10164403489305 + # 1000:10157256668464 + # 500:10157256668463 + # 300:10157256668462 + # 200:10157256668461 + # 100:10157256668460 + + skus = { + 10.0: "10186761790338", + 20.0: "10157199199546", + 100.0: "10157256668460", + 200.0: "10155862962901", + 300.0: "10164405273963", + 500.0: "10164404344795", + 1000.0: "10155867606890", + 102.0: "10157256668460", + 308.0: "10155867355510", + 520: "10155863307550", + 1026: "10155867606890", + } + if request_data.face_price not in skus: + return error( + code=BusinessCode.JD_ORDER_FACE_PRICE_ERR, + data=PlatPayResponseData( + **{ + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": "不支持的充值面额", + "face_price": 0, + } + ), + message="不支持的充值金额", + ) + try: + c_trip_spider = XiechengCardSpider( + cookies=cookies, + order_num=order_num, + sku_id=skus[request_data.face_price], + ) + code, res = await run_in_threadpool(c_trip_spider.run) + if code == BusinessCode.SUCCESS: + return success(data=res) + return error(code=code, data=res, message="请求完成") + except Exception as e: + logger.error(f"请求失败:{traceback.format_exc()}") + return error( + code=BusinessCode.INTERNAL_ERROR, + data={ + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(e), + }, + ) + case AppleStoreRequestCategoryEnum.Walmart: + skus = { + 100.0: "10140177420168", + 200.0: "10140177579817", + 300.0: "10140177668026", + 500.0: "10140177784201", + 1000.0: "10140177806944", + } + if request_data.face_price not in skus: + return error( + code=BusinessCode.JD_ORDER_FACE_PRICE_ERR, + data={}, + message="不支持的充值金额", + ) + try: + code = BusinessCode.INTERNAL_ERROR + for i in range(3): + c_trip_spider = XiechengCardSpider( + cookies=cookies, + order_num=order_num, + sku_id=skus[request_data.face_price], + ) + code, res = await run_in_threadpool(c_trip_spider.run) + if code == BusinessCode.JD_ORDER_RISK_ERR: + proxy_pool = ProxyPoolFactory.get_proxy_pool( + ProxyPoolType.EXPIRING, expire_time=60 + ) + proxy = proxy_pool.get_proxy(order_id=order_num) + if proxy: + proxy_pool.remove_invalid_proxy(proxy) + break + if code == BusinessCode.SUCCESS: + return success(data=res) + return error(code=code, data=res, message="请求完成") + except Exception as e: + logger.error(f"请求失败:{traceback.format_exc()}") + return error( + code=BusinessCode.INTERNAL_ERROR, + data={ + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(e), + }, + ) + except Exception as e: + logger.error(f"请求失败:{traceback.format_exc()}") + return error( + code=BusinessCode.INTERNAL_ERROR, + data={ + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(e), + }, + message="请求完成", + ) + + +@router.post("/query/card", response_model=ApiResponse[QueryCardResponseData]) +async def query_card(request_data: QueryCardRequest): + """查卡密""" + cookies = request_data.cookies.strip() + + # 打印参数日志 + logger.info(f"订单:{request_data}") + + try: + match request_data.category: + case AppleStoreRequestCategoryEnum.Apple: + app_store_ = AppStoreSpider( + cookies=cookies, order_num=request_data.jd_order_id + ) + code, res = await run_in_threadpool( + app_store_.query_card, request_data.jd_order_id + ) + logger.info( + f"订单号:{request_data.order_id},订单ID:{request_data.jd_order_id},最终返回日志:{res}" + ) + if code == BusinessCode.SUCCESS: + return success(data=res) + return error(code=code, data=res, message="查询完成") + case ( + AppleStoreRequestCategoryEnum.CTrip + | AppleStoreRequestCategoryEnum.Walmart + ): + c_trip_spider = XiechengCardSpider( + cookies=cookies, + order_num=request_data.jd_order_id, + sku_id=request_data.order_id, + ) + code, res = await run_in_threadpool( + c_trip_spider.get_locdetails, request_data.jd_order_id + ) + logger.info( + f"订单号:{request_data.order_id},订单ID:{request_data.jd_order_id},最终返回日志:{res}" + ) + if code == BusinessCode.SUCCESS: + return success(data=res) + return error(code=code, data=res, message="查询完成") + return error(code=BusinessCode.JD_ORDER_RISK_ERR, data=None, message="查询失败") + except Exception as e: + logger.error(f"查询卡密失败: {traceback.format_exc()}") + return error(code=BusinessCode.INTERNAL_ERROR, data=None, message="查询异常") diff --git a/apps/jd/router/auth.py b/apps/jd/router/auth.py new file mode 100644 index 0000000..5673d2c --- /dev/null +++ b/apps/jd/router/auth.py @@ -0,0 +1,41 @@ +from fastapi import APIRouter + +from apps.jd.schemas.models import SmsCodeRequest, SmsLoginRequest +from apps.jd.services.login import LoginSpider +from observability.logging import get_logger_with_trace + +router = APIRouter(prefix="/jd/sms", tags=["短信认证"]) + +logger = get_logger_with_trace(__name__) + +@router.post("/code") +async def get_code(request_data: SmsCodeRequest): + """发送短信验证码""" + phone = request_data.phone + + res = LoginSpider(phone_=phone).run_send_code() + logger.info(f"发送验证码返回:{res}") + + return res + + +@router.post("/login") +async def sms_login(request_data: SmsLoginRequest): + """短信登录""" + phone = request_data.phone + ck = request_data.ck + code = request_data.code + s_token = request_data.s_token + jd_risk_token_id = request_data.jd_risk_token_id + rsa_modulus = request_data.rsa_modulus + + res = LoginSpider(phone_=phone).run_get_ck( + ck=ck, + code=code, + s_token=s_token, + jd_risk_token_id=jd_risk_token_id, + rsa_modulus=rsa_modulus, + ) + logger.info(f"短信登录返回:{res}") + + return res diff --git a/apps/jd/router/goods.py b/apps/jd/router/goods.py new file mode 100644 index 0000000..27499d8 --- /dev/null +++ b/apps/jd/router/goods.py @@ -0,0 +1,65 @@ +from logging import LoggerAdapter +import traceback + +from fastapi import APIRouter, HTTPException + +from apps.jd.schemas.models import GoodsStoreRequest, SkuRequest +from apps.jd.services.game_area import GameArea +from apps.jd.services.goods_apple_card import GoodsAppleCard +from core.responses import BusinessCode +from observability.logging import get_logger_with_trace + + +router = APIRouter(prefix="/jd", tags=["商品管理"]) +logger: LoggerAdapter = get_logger_with_trace(__name__) + + +@router.post("/sku") +async def get_sku_details(request_data: SkuRequest): + """获取sku参数""" + cookies = request_data.cookies.strip() + sku_id = request_data.sku_id + + details = GameArea.get_details(cookies, sku_id) + data = details.get("result", {}) + item = {"code": 100 if data else 110, "data": data} + return item + + +@router.post("/goods/store") +async def goods_store(request_data: GoodsStoreRequest): + """账号类下单""" + # 参数 + face_price = request_data.face_price + order_num = request_data.order_num + cookies = request_data.cookies.strip() + brand_id = request_data.brand_id + sku_id = request_data.sku_id + username = request_data.username + game_srv = request_data.gamesrv + game_area_ = request_data.gamearea + recharge_type = request_data.recharge_type + + # 打印参数日志 + logger.info(f"订单ID:{order_num},cookies:{cookies}") + logger.info(f"订单ID:{order_num},card_pwd:{face_price}") + + try: + app_store__ = GoodsAppleCard( + cookies=cookies, + face_price=face_price, + order_num=order_num, + sku_id=sku_id, + brand_id=brand_id, + username=username, + game_srv=game_srv, + game_area=game_area_, + recharge_type=recharge_type, + ) + code, res = app_store__.run() + logger.info(f"订单ID:{order_num},goods_store最终返回日志:{res}") + if code == BusinessCode.SUCCESS: + return res + except Exception as e: + logger.error("请求失败" + traceback.format_exc(), exc_info=True) + raise HTTPException(status_code=500, detail="请求失败") diff --git a/apps/jd/router/order.py b/apps/jd/router/order.py new file mode 100644 index 0000000..b687c76 --- /dev/null +++ b/apps/jd/router/order.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter + +from apps.jd.schemas.models import DeleteOrderRequest +from apps.jd.services.delete import DeleteOrder +from observability.logging import LoggerAdapter, get_logger_with_trace +from core.responses import error + + +router = APIRouter(prefix="/api/v1/jd", tags=["订单管理"]) +logger: LoggerAdapter = get_logger_with_trace(__name__) + + +@router.post("/delete_order") +async def delete_order(request_data: DeleteOrderRequest): + """删除订单""" + # 接收参数 + cookie = request_data.cookie.strip() + order_id = request_data.order_id + + delete_res = DeleteOrder(cookie=cookie, order_id=order_id).run() + if isinstance(delete_res.get("body"), bool): + code = 2000 + else: + code = 2001 + return error(code=code, data={}, message="请求成功") # type: ignore diff --git a/apps/jd/router/payment.py b/apps/jd/router/payment.py new file mode 100644 index 0000000..3c67e19 --- /dev/null +++ b/apps/jd/router/payment.py @@ -0,0 +1,96 @@ +import traceback + +from fastapi import APIRouter, HTTPException +from starlette.concurrency import run_in_threadpool + +from apps.jd.schemas.models import ( + PlatPayRequest, + PlatPayResponseData, + RefreshPaymentRequest, +) +from apps.jd.services.app_store import AppStoreSpider +from apps.jd.services.ctrip import XiechengCardSpider +from core.responses import ApiResponse, BusinessCode, success, error +from observability.logging import get_logger_with_trace + + +router = APIRouter(prefix="/order", tags=["支付"]) +logger = get_logger_with_trace(__name__) + + +# @router.post("/plat_pay_channel", response_model=ApiResponse) +# async def plat_pay_channel(request_data: PlatPayRequest): +# """获取微信app端支付参数""" +# # 接收参数 +# order_id = request_data.order_id +# jd_order_id = request_data.jd_order_id +# face_price = float(request_data.face_price) +# pay_id = request_data.pay_id +# cookies = request_data.cookies.strip() + +# # 打印日志 +# logger.info( +# f"获取微信app端支付参数 cookie:{cookies} order_id:{order_id} face_price:{face_price} pay_id:{pay_id}" +# ) + +# try: +# apple_store_spider = AppStoreSpider( +# cookies=cookies, +# order_num=order_id, +# face_price=face_price, +# ) +# code, deeplink = await run_in_threadpool( +# apple_store_spider.ios_pay, jd_order_id, pay_id +# ) +# data = PlatPayResponseData( +# deeplink=deeplink, order_id=order_id, pay_id=pay_id, face_price=face_price +# ) +# return success(code=getattr(code, "value", code), data=data, msg="请求成功") +# except Exception as e: +# logger.error("请求失败" + traceback.format_exc(), exc_info=True) +# raise HTTPException(status_code=500, detail="请求失败") + + +@router.post("/refresh-payment", response_model=ApiResponse[PlatPayResponseData]) +async def refresh_payment(request_data: RefreshPaymentRequest): + """获取微信app端支付参数""" + # 打印日志 + logger.info( + f"获取微信app端支付参数 cookie:{request_data.cookies} order_id:{request_data.order_id} pay_id:{request_data.pay_id} user_order_id:{request_data.user_order_id}" + ) + # data = { + # "deeplink": "weixin://wap/pay?prepayid%3Dwx11141602253864f3a03c1c654daa820001&package=3012187903&noncestr=1760163364&sign=b8e94df5e28a10e0938d84e322161882", + # "order_id": "339328433580", + # "pay_id": "652e4edb825348e78f692c17ef1bc8b9", + # "face_price": 0 + # } + # return my_json(code=100, data=data, msg="请求完成") + res = {} + try: + for i in range(3): + c_trip_spider = XiechengCardSpider( + cookies=request_data.cookies, + order_num=request_data.user_order_id, + sku_id="", + ) + code, res = await run_in_threadpool( + c_trip_spider.refresh_payment_url, + request_data.pay_id, + request_data.order_id, + ) + if code == BusinessCode.JD_ORDER_RISK_ERR: + continue + if code == BusinessCode.SUCCESS: + return success(data=res) + return error( + code=BusinessCode.JD_ORDER_RISK_ERR, + data=PlatPayResponseData(**{ + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": res.get("remark"), + }), + ) + except Exception as e: + logger.error(f"请求失败:{traceback.format_exc()}") + raise HTTPException(status_code=500, detail="请求失败") diff --git a/apps/jd/schemas/models.py b/apps/jd/schemas/models.py new file mode 100644 index 0000000..c5dcf27 --- /dev/null +++ b/apps/jd/schemas/models.py @@ -0,0 +1,110 @@ +from enum import Enum + +from pydantic import BaseModel, Field + + +class AppleStoreRequestCategoryEnum(Enum): + Apple = "apple" + CTrip = "cTrip" + Walmart = "walmart" + + +class AppStoreRequest(BaseModel): + """苹果权益充值请求""" + + category: AppleStoreRequestCategoryEnum = Field(..., description="分类") + face_price: float = Field(..., description="面值") + order_num: str = Field(..., description="订单号") + cookies: str = Field(..., description="登录cookies") + + +class QueryCardRequest(BaseModel): + """查卡密请求""" + + category: AppleStoreRequestCategoryEnum = Field(..., description="分类") + order_id: str = Field(..., description="订单号") + jd_order_id: str = Field(..., description="京东订单号") + cookies: str = Field(..., description="登录cookies") + + +class SkuRequest(BaseModel): + """获取SKU参数请求""" + + cookies: str = Field(..., description="登录cookies") + sku_id: str = Field(..., description="SKU ID") + + +class GoodsStoreRequest(BaseModel): + """账号类下单请求""" + + face_price: float = Field(..., description="面值") + order_num: str = Field(..., description="订单号") + cookies: str = Field(..., description="登录cookies") + brand_id: str = Field(..., description="品牌ID") + sku_id: str = Field(..., description="SKU ID") + username: str | None = Field(None, description="用户名") + gamesrv: str | None = Field(None, description="游戏服务器") + gamearea: str | None = Field(None, description="游戏区域") + recharge_type: int = Field( + 0, description="充值类型 1:username账号充值,2:游戏区服充值" + ) + + +class PlatPayRequest(BaseModel): + """支付渠道请求""" + + order_id: str = Field(..., description="订单ID") + jd_order_id: str = Field(..., description="京东订单ID") + face_price: float = Field(..., description="面值") + pay_id: str = Field(..., description="支付ID") + cookies: str = Field(..., description="登录cookies") + + +class RefreshPaymentRequest(BaseModel): + """刷新支付请求""" + + user_order_id: str = Field(..., description="用户订单ID") + pay_id: str = Field(..., description="支付ID") + cookies: str = Field(..., description="登录cookies") + order_id: int = Field(..., description="订单ID") + + +class DeleteOrderRequest(BaseModel): + """删除订单请求""" + + cookie: str = Field(..., description="登录cookies") + order_id: str = Field(..., description="订单ID") + + +class SmsCodeRequest(BaseModel): + """发送短信验证码请求""" + + phone: str = Field(..., description="手机号") + + +class SmsLoginRequest(BaseModel): + """短信登录请求""" + + phone: str = Field(..., description="手机号") + ck: str = Field(..., description="cookies") + code: str = Field(..., description="验证码") + s_token: str = Field(..., description="s_token") + jd_risk_token_id: str = Field(..., description="京东风控token ID") + rsa_modulus: str = Field(..., description="RSA模数") + + +class PlatPayResponseData(BaseModel): + deeplink: str = Field("", description="微信唤起链接或支付信息") + order_id: str = Field("", description="订单号") + remark: str | None = Field(None, description="备注") + pay_id: str = Field("", description="支付ID") + face_price: float = Field(0.0, description="面值") + + +class QueryCardResponseData(BaseModel): + """查卡密响应数据""" + + order_status: str = Field(..., description="订单状态") + card_num: str = Field(..., description="卡号") + card_pwd: str = Field(..., description="卡密") + remark: str = Field(default="", description="备注") \ No newline at end of file diff --git a/apps/jd/services/app_store.py b/apps/jd/services/app_store.py new file mode 100644 index 0000000..c073d0d --- /dev/null +++ b/apps/jd/services/app_store.py @@ -0,0 +1,773 @@ +import base64 +import hashlib +import json +import platform +import random +import re +import time +import traceback +import uuid +from urllib import parse +from curl_cffi import requests +from fake_useragent import UserAgent +from tenacity import retry, stop_after_attempt, wait_exponential + +from apps.jd.schemas.models import QueryCardResponseData +from apps.jd.services.jstk import NormalJsTk, TxSMJsTk +from apps.jd.services.utils import gen_cipher_ep, get_pay_sign, get_sign +from apps.shared.proxy_pool.proxy_pool import ProxyPoolFactory +from core.config import ProxyPoolType, settings +from core.responses import BusinessCode +from observability.logging import LoggerAdapter, get_logger_with_trace + +# +logger: LoggerAdapter = get_logger_with_trace(__name__) + + +class AppStoreSpider: + def __init__( + self, + cookies, + order_num, + face_price: float = 0, + user_client: str = UserAgent().random, + ): + self.SKU_MA = { + "10.00": 10022039398507, + "50.00": 11170365589, + "100.00": 11183343342, + "200.00": 11183368356, + "500.00": 11183445154, + "1000.00": 10066407336810, + "68.00": 10023403480808, + "00.00": 10026503885976, + } + self.__proxy_pool = ProxyPoolFactory.get_proxy_pool(settings.proxy_type) + self.order_num = order_num + self.cookies = cookies + self.face_price = face_price + self.__user_client = user_client + self.md5_key = "e7c398ffcb2d4824b4d0a703e38eb0bb" + self.time_stamp = int(time.time() * 1000) + self.submit_order_url = "https://api.m.jd.com/appstore/submitorder" + self.ticket_url = "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkcaptcha" + self.jd_api = "https://api.m.jd.com/api" + self.action_url = "https://api.m.jd.com/client.action" + self.check_captcha_url = ( + "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkcaptcha" + ) + self.headers = { + "content-type": "application/x-www-form-urlencoded", + "cookie": self.cookies, + "origin": "https://txsm-m.jd.com", + "referer": "https://txsm-m.jd.com/", + "user-agent": self.get_user_agent(), + } + self.current_os = platform.system() + self.js = None + self.__js_tk = TxSMJsTk() + self.__js_tk.generate_token(self.get_user_agent(), cookies) + expiring_pool = ProxyPoolFactory.get_proxy_pool( + ProxyPoolType.EXPIRING, expire_time=60 + ) + self.proxy = expiring_pool.get_proxy(order_id=uuid.uuid4().hex) + # self.init_js() + + def h5st(self, pt_pin: str, app_id: str, user_agent: str, data: dict): + logger.info(f"h5st: {pt_pin}, {app_id}, {user_agent}, {data}") + response = requests.post( + "http://152.136.211.112:9001/h5st", + json={ + "appCode": "Z6eXo8Dl", + "pin": pt_pin, + "ua": user_agent, + "body": data, + "appId": app_id, + }, + ) + return response.json().get("body", {}).get("h5st", {}).get("h5st", "") + + def get_user_agent(self) -> str: + if "iPhone" in self.__user_client: + return "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Mobile/15E148 Safari/604.1" + return f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/{random.randint(120, 140)}.0.0.0" + + def get_eid_token(self): + return self.__js_tk.get_token() + + def format_number(self, number): + if not number: + number = 0 + number = float(number) + formatted_number = "{:.2f}".format(number) + return formatted_number + + def get_s(self): + use_bean = 0 + buy_num = 1 + face_price = self.face_price + type = 1 + brand_id = 999440 + eid = self.__js_tk.get_eid() + pay_mode = 0 + coupon_ids = "" + sku_id = self.SKU_MA.setdefault( + self.format_number(self.face_price), 10026503885976 + ) + total_price = self.format_number(self.face_price) + order_source = 2 + order_source_type = 2 + s = f"{use_bean}{buy_num}{face_price}{type}{brand_id}{eid}{pay_mode}{coupon_ids}{sku_id}{total_price}{order_source}{order_source_type}" + return s + + def get_u(self): + u = f"{self.time_stamp}{self.md5_key}" + return u + + def get_enc_str(self): + s = self.get_s() + u = self.get_u() + _str = f"{s}{u}" + enc_str = hashlib.md5(_str.encode()).hexdigest() + return enc_str + + def get_body(self, enc_str): + item = { + "useBean": 0, + "buyNum": 1, + "facePrice": self.face_price, + "type": 1, + "brandId": 999440, + "eid": self.__js_tk.get_eid(), + "payMode": "0", + "couponIds": "", + "skuId": self.SKU_MA.setdefault( + self.format_number(self.face_price), 10026503885976 + ), + "totalPrice": self.format_number(self.face_price), + "orderSource": 2, + "orderSourceType": 2, + "t": self.time_stamp, + "channelSource": "txzs", + "encStr": enc_str, + "babelChannel": "ttt35", + } + json_string = json.dumps(item) + encrypted_message = ( + base64.b64encode(json_string.encode("utf-8")).decode("utf-8").rstrip("=") + ) + return encrypted_message + + def submit_order(self, body): + data = { + "appid": "txsm-m", + "client": "iPhone", + "functionId": "appstore_order_submit_new", + "uuid": uuid.uuid4().hex.replace("-", ""), + "osVersion": "16.6", + "screen": "1170.000046491623*2532.0001006126404", + "t": self.time_stamp, + "loginType": "2", + "x-api-eid-token": self.__js_tk.get_eid(), + "body": body, + } + proxy = self.__proxy_pool.get_proxy(order_id=self.order_num) + if proxy: + response = requests.post( + url=self.submit_order_url, + headers=self.headers, + data=data, + proxies={"http": proxy, "https": proxy}, + ) + else: + response = requests.post( + url=self.submit_order_url, headers=self.headers, data=data + ) + + return response.json() + + @retry( + stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=15) + ) + def get_ticket_res(self): + headers = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Language": "zh-CN,zh;q=0.9", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Pragma": "no-cache", + "Upgrade-Insecure-Requests": "1", + "User-Agent": self.get_user_agent(), + } + url = "http://ticket_slide_server:99/api/TX?aid=2093769752&host=https://t.captcha.qq.com&ip=" + proxy = self.__proxy_pool.get_proxy(order_id=self.order_num) + if proxy: + response = requests.get( + url, + headers=headers, + verify=False, + proxies={"http": proxy, "https": proxy}, + ) + else: + response = requests.get(url, headers=headers, verify=False) + return response.json() + + def decrypt_card_info(self, message): + key = "2E1ZMAF88CCE5EBE551FR3E9AA6FF322" + card_info = self.js.call("decryptDes", message, key) + card_info = json.loads(card_info) + return card_info[0] + + def get_card_res(self, order_id: str): + """ + 获取订单卡号/卡密详情 + + :param order_id: 订单号 + :return: 卡号/卡密详情 + """ + user_agent = self.get_user_agent() + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://recharge.m.jd.com", + "priority": "u=1, i", + "referer": "https://recharge.m.jd.com/", + "user-agent": user_agent, + "x-referer-page": "https://recharge.m.jd.com/orderDetail", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookies, + } + token_service = NormalJsTk() + token_service.generate_token(user_agent, self.cookies) + data = { + "appid": "tsw-m", + "functionId": "getGPOrderDetail", + "t": self.time_stamp, + "body": '{"appKey":"android","source":41,"orderId":"%s","version":"1.10","rechargeversion":"12.8","moduleName":"JDReactVirtualRecharge","apiVersion":"new"}' + % (order_id), + "uuid": "1736139761886772644158", + "screen": "2560.5*1600.5", + "x-api-eid-token": token_service.get_token(), + } + cookie_dict: dict[str, str] = { + cookie.split("=")[0].strip(): cookie.split("=")[1].strip() + for cookie in self.cookies.split(";") + } + h5st = self.h5st(cookie_dict.get("pt_pin", ""), "8e94a", user_agent, data) + data["h5st"] = h5st + response = requests.post(self.jd_api, headers=headers, data=data) + return response.json() + + def get_pay_res(self, order_id): + headers = { + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "content-type": "application/json;charset=UTF-8", + "origin": "https://trade.m.jd.com", + "priority": "u=1, i", + "referer": "https://trade.m.jd.com/", + "sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": self.get_user_agent(), + "x-referer-page": "https://trade.m.jd.com/order/orderlist_jdm.shtml", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookies, + } + params = { + "t": f"{self.time_stamp}", + "loginType": "2", + "loginWQBiz": "golden-trade", + "appid": "m_core", + "client": "MacIntel", + "clientVersion": "", + "build": "", + "osVersion": "null", + "screen": "1920*1080", + "networkType": "4g", + "partner": "", + "forcebot": "", + "d_brand": "", + "d_model": "", + "lang": "zh-CN", + "scope": "", + "sdkVersion": "", + "openudid": "", + "uuid": uuid.uuid4().hex.replace("-", ""), + "x-api-eid-token": self.__js_tk.get_token(), + "functionId": "pay_info_m", + "body": '{"appType":3,"bizType":"2","deviceUUId":"","platform":3,"sceneval":"2","source":"m_inner_myJd.orderFloor_orderlist","systemBaseInfo":"{\\"pixelRatio\\":2,\\"screenWidth\\":1920,\\"screenHeight\\":1080,\\"windowWidth\\":1920,\\"windowHeight\\":959,\\"statusBarHeight\\":null,\\"safeArea\\":{\\"bottom\\":0,\\"height\\":0,\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"width\\":0},\\"bluetoothEnabled\\":false,\\"locationEnabled\\":false,\\"wifiEnabled\\":false,\\"deviceOrientation\\":\\"landscape\\",\\"benchmarkLevel\\":-1,\\"brand\\":\\"\\",\\"model\\":\\"\\",\\"system\\":null,\\"platform\\":\\"MacIntel\\",\\"SDKVersion\\":\\"\\",\\"enableDebug\\":false,\\"language\\":\\"zh-CN\\",\\"version\\":\\"\\",\\"theme\\":\\"light\\",\\"fontSizeSetting\\":null,\\"albumAuthorized\\":false,\\"cameraAuthorized\\":false,\\"locationAuthorized\\":false,\\"microphoneAuthorized\\":false,\\"notificationAuthorized\\":false,\\"notificationAlertAuthorized\\":false,\\"notificationBadgeAuthorized\\":false,\\"notificationSoundAuthorized\\":false,\\"phoneCalendarAuthorized\\":false,\\"locationReducedAccuracy\\":false,\\"environment\\":\\"\\"}","orderId":"%s","origin":10,"tenantCode":"jgm","bizModelCode":"2","bizModeClientType":"M","bizModeFramework":"Taro","externalLoginType":1,"token":"3852b12f8c4d869b7ed3e2b3c68c9436","appId":"m91d27dbf599dff74"}' + % order_id, + "h5st": "20250513003554680%3Bg9gwwg93pzmtgwp3%3B9b070%3Btk03wac841c0518nh3UsKG1ttEEV8mnlrHkEhynQ-S2x3PbyeW6t_Cn0S29bQlTaYFu10XQnqBTZqpZVg7yWDGL1-Ien%3B57ceff3efad640ff671169173de4393cea87add0aeafeb28fe45d8ed0885f34d%3B4.2%3B1747067754680%3Be2dbf31f18c0566b03779cdfb7daf1889536ac5980ca54061cc5d12b276f6fcb74aa49e97fb21499ccffae2bb79d16ba8b664ce42ae53fce6b12c709789cf1eeb1e039f7a491fa6c0bb41380e593285cdb5cbaca0dd43586ff7937f44c9f6f0a1104467ba19dc3d8a5081e3bf7da385fee7ad469c515d6cd459c9efc82eb331e899e6c338ec36e3cfcf4466efd3d4bb31f752f3730e1343447f0524cfabc0dd22d9dbc853d384e6f6969ac7eb9339b72b5ff17147816fa422a79b375f28edbb490089ee8dd463b06339ba1536ba3e4ad90d82913ad4bd6049ff583666f24d648bb47c6d2f65380c4bda6ac0eea24fc622fa95763f9352247d21980f0b8dcdc54d7a2d18b84dad43660f88fcfb8c8613d86daa87f58addf1a60f3df6e5626753bf497864d03408578ff78bd5007b1bf86f0747b05681c58b32d27ae7b2de07059", + } + proxy = self.__proxy_pool.get_proxy(order_id=self.order_num) + if proxy: + response = requests.get( + self.action_url, + headers=headers, + params=params, + proxies={"http": proxy, "https": proxy}, + ) + else: + response = requests.get(self.action_url, headers=headers, params=params) + return response.json() + + def plat_pay_channel_res(self, pay_id): + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://mpay.m.jd.com", + "priority": "u=1, i", + "referer": "https://mpay.m.jd.com/", + "sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": self.get_user_agent(), + "x-referer-page": "https://mpay.m.jd.com/mpay.623f9498223cf9b9de9f.html", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookies, + } + params = {"functionId": "platPayChannel", "appid": "mcashier", "scval": "mpay"} + data = { + "body": '{"appId":"m_D1vmUq63","payId":"%s","source":"mcashier","origin":"h5","mcashierTraceId":1729842386189}' + % (pay_id), + "x-api-eid-token": self.__js_tk.get_token(), + "h5st": "20241025154626341;0587023779148689;303a7;tk03w7da11b8b18nOQ6HZGNLj6DdtLQBS695YHMu7RyONolcwWCRc8ihMUs5ITCem6HIGhdYo_DpJ62yYLkdrxIBxE0N;ab8a935407baae929b0d3e267f67693f9c00a5d876ad149c10e904371f87726f;3.1;1729842386341;24c9ee85e67cf80746dd82817ecbeafc7a829b35c7f446a4c7d476cc9faa1d8834a93323ad7bce9bef1bba682b93d2e3694e425ff68d304875c1ae9e2ae398cfd94e4ff03cd3bdd9f0f600a0d75c92d537baaa944d39072a92db7dc20c99e7f80889e289e78a1f8f93c57f8471890c464b78b61e9b3bbffea712e6d6c671ad12", + } + proxy = self.__proxy_pool.get_proxy(order_id=self.order_num) + if proxy: + response = requests.post( + self.action_url, + headers=headers, + params=params, + data=data, + proxies={"http": proxy, "https": proxy}, + ) + else: + response = requests.post( + self.action_url, headers=headers, params=params, data=data + ) + return response.json() + + def plat_wx_pay_res(self, pay_id): + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://mpay.m.jd.com", + "priority": "u=1, i", + "referer": "https://mpay.m.jd.com/", + "sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": self.get_user_agent(), + "x-referer-page": "https://mpay.m.jd.com/mpay.623f9498223cf9b9de9f.html", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookies, + } + params = {"functionId": "platWapWXPay", "appid": "mcashier", "scval": "mpay"} + data = { + "body": '{"appId":"m_D1vmUq63","payId":"%s","eid":"PBZHV4O4RF5SAA7QGYPZEPRPAYOCCF3WTUQYMWEFASLCJNYX2HWO7C35L5TYQUL66FGXVVMXDWKTBEEE24LW42XEWM","source":"mcashier","origin":"h5","mcashierTraceId":1729837716957}' + % pay_id, + "x-api-eid-token": "jdd03PBZHV4O4RF5SAA7QGYPZEPRPAYOCCF3WTUQYMWEFASLCJNYX2HWO7C35L5TYQUL66FGXVVMXDWKTBEEE24LW42XEWMAAAAMSYJMLE3IAAAAAC5OTMMGGUM5SYIX", + # "h5st": "20241025142845015;0587023779148689;303a7;tk03w7da11b8b18nOQ6HZGNLj6DdtLQBS695YHMu7RyONolcwWCRc8ihMUs5ITCem6HIGhdYo_DpJ62yYLkdrxIBxE0N;48aee025d65fab059f98c546832f7a1d7bca99dba6bc0df9bd6328b9817cf394;3.1;1729837725015;24c9ee85e67cf80746dd82817ecbeafc7a829b35c7f446a4c7d476cc9faa1d8834a93323ad7bce9bef1bba682b93d2e3694e425ff68d304875c1ae9e2ae398cfd94e4ff03cd3bdd9f0f600a0d75c92d537baaa944d39072a92db7dc20c99e7f80889e289e78a1f8f93c57f8471890c464b78b61e9b3bbffea712e6d6c671ad12" + } + proxy = self.__proxy_pool.get_proxy(order_id=self.order_num) + if proxy: + response = requests.post( + self.action_url, + headers=headers, + params=params, + data=data, + proxies={"http": proxy, "https": proxy}, + ) + else: + response = requests.post( + self.action_url, headers=headers, params=params, data=data + ) + return response.json() + + def get_deep_link_res(self, mweb_url): + headers = { + "Host": "wx.tenpay.com", + "sec-ch-ua-platform": '"Windows"', + "User-Agent": self.get_user_agent(), + "sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"', + "sec-ch-ua-mobile": "?0", + "Accept": "*/*", + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + "Referer": mweb_url, + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + } + prepay_id = re.search(r"prepay_id=(wx\w+)", mweb_url).group(1) + package = re.search(r"package=(\d+)", mweb_url).group(1) + ticket_res = self.get_ticket_res() + logger.info(f"订单号:{self.order_num},获取ticket返回:{ticket_res}") + ticket = ticket_res["ticket"] + randstr = ticket_res["randstr"] + params = { + "ticket": ticket, + "randstr": randstr, + "prepayid": prepay_id, + "package": package, + } + proxy = self.__proxy_pool.get_proxy(order_id=self.order_num) + if proxy: + response = requests.get( + self.check_captcha_url, + headers=headers, + params=params, + proxies={"http": proxy, "https": proxy}, + ) + else: + response = requests.get( + self.check_captcha_url, headers=headers, params=params + ) + return response.json() + + def get_card_secret(self, jd_order_num): + card_res = self.get_card_res(jd_order_num) + logger.info(f"获取卡密信息返回:{card_res}") + if not card_res: + return 110, card_res + if card_res.get("code") != "0": + return 110, card_res + card_info = card_res.get("result").get("cardInfos") + if not card_info: + return 110, card_res + card_info = self.decrypt_card_info(card_info) + return 100, card_info + + def query_card(self, jd_order_num): + """ + 查询订单状态和卡密信息 + + :param jd_order_num: 订单号 + :return: code=100成功,包含order_status, card_num, card_pwd字段 + """ + try: + card_res = self.get_card_res(jd_order_num) + logger.info(f"获取卡密信息返回:{card_res}") + + if not card_res: + return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData( + order_status="", card_num="", card_pwd="", remark=str(card_res) + ) + # 判断没有登录的场景 + if card_res.get("code") == "20001": + return BusinessCode.JD_ORDER_CK_ERR, QueryCardResponseData( + order_status="", card_num="", card_pwd="", remark=str(card_res) + ) + if card_res.get("code") != "0": + return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData( + order_status="", card_num="", card_pwd="", remark=str(card_res) + ) + + card_info_list = card_res.get("result", {}).get("cardInfos", []) + if not card_info_list: + return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData( + order_status="获取失败", + card_num="", + card_pwd="", + remark=str(card_res), + ) + + try: + card_info = self.decrypt_card_info(card_info_list) + return BusinessCode.SUCCESS, QueryCardResponseData( + order_status=card_info.get("status", "成功"), + card_num=card_info.get("cardNo", ""), + card_pwd=card_info.get("cardPwd", ""), + remark=str(card_res), + ) + except Exception as e: + logger.error(f"解密卡密信息失败: {e}") + return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData( + order_status="解密失败", card_num="", card_pwd="" + ) + + except Exception as e: + logger.error(f"查询卡密信息异常: {e}") + return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData( + order_status="查询失败", card_num="", card_pwd="" + ) + + def run(self) -> tuple[BusinessCode, dict, str]: + # 获取加密参数 + try: + enc_str = self.get_enc_str() + except KeyError as e: + return BusinessCode.JD_ORDER_FACE_PRICE_ERR, {}, "充值面值有误" + # 获取请求body + body = self.get_body(enc_str) + # 提交预付款订单 + order_res = self.submit_order(body) + # 提交预付款订单 + logger.info( + f"订单号:{self.order_num},app_store提交预付款订单返回:{order_res}" + ) + if order_res.get("code") == 300: + return BusinessCode.JD_ORDER_CK_ERR, {}, str(order_res) + if order_res.get("code") != 200: + return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(order_res) + + # 提交预付款订单 + # # 获取支付信息 + pay_res = self.get_pay_res(order_res["data"]) + logger.info(f"订单号:{self.order_num},app_store获取支付信息返回:{pay_res}") + # pay_res = {'body': {'payId': '0d02ea36cf7946a89ed6f51898d2d2a3', 'url': 'https://mpay.m.jd.com/mpay.25f73d1adb414ba7e0e3.html?appId=m_D1vmUq63&payId=0d02ea36cf7946a89ed6f51898d2d2a3&orderId=314518542769&tId=mpay'}, 'code': '0', 'message': 'success', 'timestamp': 1747127287841} + if pay_res.get("code") != "0": + return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(pay_res) + + # 获取支付信息 + pay_id = pay_res["body"]["payId"] + # 获取微信支付信息 + pay_channel_res = self.plat_pay_channel_res(pay_id) + logger.info( + f"订单号:{self.order_num},app_store请求微信渠道返回:{pay_channel_res}" + ) + if pay_channel_res.get("code") != "0": + return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(pay_channel_res) + + if "iPhone" in self.get_user_agent(): + code, msg = self.ios_pay(pay_channel_res["orderId"], pay_id) + data = {} + else: + code, data, msg = self.web_pay(pay_id) + if code != BusinessCode.SUCCESS: + return code, data, msg + + return ( + code, + { + "deeplink": msg, + "order_id": pay_channel_res["orderId"], + "pay_id": pay_id, + "face_price": self.face_price, + }, + "请求成功", + ) + + def web_pay(self, pay_id: str) -> tuple[BusinessCode, dict, str]: + # 获取微信支付信息 + wx_pay_res = self.plat_wx_pay_res(pay_id) + logger.info( + f"订单号:{self.order_num},app_store获取微信支付信息返回:{wx_pay_res}" + ) + if wx_pay_res.get("code") != "0": + return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(wx_pay_res) + if wx_pay_res.get("errorCode") == "-1": + wx_pay_res["order_id"] = self.order_num + wx_pay_res["pay_id"] = pay_id + wx_pay_res["face_price"] = self.face_price + return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(wx_pay_res) + mweb_url = wx_pay_res["payInfo"]["mweb_url"] + # 获取支付链接信息 + deep_link_res = self.get_deep_link_res(mweb_url) + logger.info( + f"订单号:{self.order_num},app_store获取支付链接deep_link信息返回:{deep_link_res}" + ) + if deep_link_res.get("retcode") != 1: + return BusinessCode.JD_ORDER_NORMAL_ERR, {}, str(deep_link_res) + return BusinessCode.SUCCESS, {}, deep_link_res["deeplink"] + + def ios_pay(self, order_id: str, pay_id: str) -> tuple[BusinessCode, str]: + headers = { + "Host": "api.m.jd.com", + "charset": "UTF-8", + "user-agent": "okhttp/3.12.1;jdmall;android;version/11.1.0;build/98139", + "cache-control": "no-cache", + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "cookie": self.cookies, + } + url = "https://api.m.jd.com/client.action" + func = "platPayChannel" + version = "11.1.0" + uuid_ = uuid.uuid4().hex.replace("-", "") + ts = int(time.time() * 1000) + ep = gen_cipher_ep(uuid_, ts) + pay_sign = get_pay_sign(order_id=order_id, face_price=self.face_price) + body = ( + '{"appId":"jd_android_app4","client":"android","fk_aid":"%s","fk_appId":"com.jingdong.app.mall","fk_latitude":"NfUaIOrNMes=","fk_longtitude":"NfUaIOrNMes=","fk_terminalType":"02","fk_traceIp":"26.26.26.1","hasCyberMoneyPay":"0","hasHuaweiPay":"0","hasOCPay":"0","hasUPPay":"0","orderId":"%s","orderPrice":"%s","orderType":"37","orderTypeCode":"0","origin":"native","payId":"%s","paySign":"%s","paySourceId":"2","payablePrice":"%s","sdkToken":"jdd016CSEHGQ3IPOGEXVOBUPDLTBSKZZUMESDDOP4PPRC2E2R7CC2K4LXQN63E6A3P3Y76GN4M5TMPIZGOWYQJG4MO6ET75VQFVQL2MIYRFQ01234567","source":"jdapp","style":"normal","supportNFC":"1"}' + % ( + uuid_, + order_id, + self.face_price, + pay_id, + pay_sign, + self.face_price, + ) + ) + formatted_params = get_sign(func, body, uuid_, version) + params = { + "functionId": func, + "clientVersion": version, + "build": "98139", + "client": "android", + "partner": "wandoujia", + # "eid": "eidAf760812200s2xOZPVA0sThGrvhABq1zrPMTmUOM1tv8FFg1FjE8yRJtYdV/UFhJuIkVZbrk/xl XPeoFQTNgMiYJpXeeyACCVPbt0/3R7R3Gd 8y", + "sdkVersion": "28", + "lang": "zh_CN", + "harmonyOs": "0", + "networkType": "wifi", + # "uts": "0f31TVRjBSsqndu4/jgUPz6uymy50MQJ8fZr7wet7pLPYx9jEXMpd8VCD64sq/eBbsH5zZBXnKZMkN1vxnjOrpfx7GiQBINsuAELLpjOiZsCHkTDoRW/d9talOxyn2bo1YZLq8uq5Kdx/Fd7diA023Qwr+5V5TzeOnca3cc6QzMFh8p+DS2gR6Bimgz0BiNqbeN1q1NA9rJ/t5QOAIH9EA==", + "uemps": "0-0", + "ext": '{"prstate":"0","pvcStu":"1"}', + "ef": "1", + "ep": ep, + "st": formatted_params["st"], + "sign": formatted_params["sign"], + "sv": formatted_params["sv"], + } + data = {"body": body, "": ""} + proxy = self.__proxy_pool.get_proxy(order_id=self.order_num) + if proxy: + response = requests.post( + url=url, + params=params, + headers=headers, + data=data, + proxies={"http": proxy, "https": proxy}, + ) + else: + response = requests.post(url=url, params=params, headers=headers, data=data) + client_res = response.json() + logger.info(f"订单id:{self.order_num},获取支付参数res返回:{client_res}") + # 不支持该支付类型 + if client_res.get("errorCode") == "-2": + return BusinessCode.JD_ORDER_TYPE_NOT_SUPPORTED_ERR, "订单类型不支持" + if client_res.get("errorCode") == "3": + return BusinessCode.JD_ORDER_CK_ERR, "ck失效" + # 订单失效 + if client_res.get("errorCode") == "-3": + return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单失效" + # 订单与下单账号不同 + if client_res.get("errorCode") == "-5": + return BusinessCode.JD_ORDER_NORMAL_ERR, "订单与下单账号不匹配" + # 订单已取消 + if client_res.get("errorCode") == "-100": + return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单已支付完成或已取消" + # 订单已支付完成或已取消 + if client_res.get("mcashierConfirmInfo", {}).get("errorCode", "") == "-100": + return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单已支付完成或已取消" + # 获取微信支付参数 + url = "https://api.m.jd.com/client.action" + headers = { + "Host": "api.m.jd.com", + "charset": "UTF-8", + "user-agent": self.get_user_agent(), + "cache-control": "no-cache", + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "cookie": self.cookies, + } + + uuid_ = uuid.uuid4().hex.replace("-", "") + ts = int(time.time() * 1000) + ep = gen_cipher_ep(uuid_, ts) + pay_sign = get_pay_sign(order_id=order_id, face_price=self.face_price) + sdk_token = "jdd01XYE6SS5ZXXG7F74OZC3PKG5LWF7W3GTVLTSUH56A2YAUXOT6NEEX4TRT3I3XDDYO7NQSIV4BSK3XGKWMROTBRKO4ENETDD4IEHFWZHA01234567" + body = ( + '{"appId":"jd_android_app4","backUrl":"","client":"android","orderId":"%s","orderPrice":"%s","orderType":"37","orderTypeCode":"0","origin":"native","payId":"%s","paySign":"%s","sdkToken":"%s","source":"jdapp"}' + % (order_id, self.face_price, pay_id, pay_sign, sdk_token) + ) + func = "platWXPay" + version = "11.0.0" + formatted_params = get_sign(func, body, uuid_, version) + params = { + "functionId": "platWXPay", + "clientVersion": "11.0.0", + # "build": "97235", + "client": "android", + "partner": "huawei", + # "eid": "eidAd2c08121b3s9eOeub59TR2G1LS3GtRpmFHjnr/5tBkXKUdbZOahaF5ejedAIDQFfwVV2GDYUzftxnEQRjdhD5bpPNR4B0qK9tPJ 5liDjiHAhDR3", + "sdkVersion": "28", + "lang": "zh_CN", + "harmonyOs": "0", + "networkType": "wifi", + # "uts": "0f31TVRjBSsqndu4/jgUPz6uymy50MQJVJk5AARu8sQVESbyLWRZZNMf5XbN+023PgE2PL4I0aLSvISDbN3u1a1oIB1KTvzrqacX+46wkkSjXxvvYKogR9YCbfnMf2pdC5H/VDcJc2u6uHTxPVwvhLoJ8vX8cN45ZijAbikou9B5o2KTvMTzCfrSYgi3+mls/cA+6k+Ao5sFIIdtimZ6bw==", + "uemps": "0-0", + "ext": '{"prstate":"0","pvcStu":"1"}', + "ef": "1", + "ep": ep, + "st": formatted_params["st"], + "sign": formatted_params["sign"], + "sv": formatted_params["sv"], + } + data = { + "body": body, + } + pay_channel_res = None + for i in range(3): + try: + proxy = self.__proxy_pool.get_proxy(order_id=self.order_num) + if proxy: + response = requests.post( + url=url, + params=params, + headers=headers, + data=data, + proxies={"http": proxy, "https": proxy}, + timeout=2, + ) + else: + response = requests.post( + url=url, params=params, headers=headers, data=data, timeout=2 + ) + pay_channel_res = response.json() + logger.info( + f"订单id:{self.order_num},获取微信支付参数返回:{pay_channel_res}" + ) + break + except Exception as e: + logger.error(traceback.format_exc()) + continue + if not pay_channel_res: + return BusinessCode.JD_ORDER_NORMAL_ERR, "爬虫异常" + # 当前支付方式不可用,建议选择其他支付方式 + if pay_channel_res.get("errorCode") == "-1": + return BusinessCode.INTERNAL_ERROR, "爬虫异常" + # 加密算法失效 + if pay_channel_res.get("code") == "600": + return BusinessCode.INTERNAL_ERROR, "加密算法异常" + # 不支持该支付类型 + if pay_channel_res.get("errorCode") == "-2": + return BusinessCode.JD_ORDER_TYPE_NOT_SUPPORTED_ERR, "订单类型不支持" + # ck失效 + if pay_channel_res.get("errorCode") == "3": + return BusinessCode.JD_ORDER_CK_ERR, "ck失效" + # 订单失效 + if pay_channel_res.get("errorCode") == "-3": + return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单失效" + # 订单与下单账号不同 + if pay_channel_res.get("errorCode") == "-5": + return BusinessCode.JD_ORDER_NORMAL_ERR, "订单与下单账号不匹配" + # 订单已取消 + if pay_channel_res.get("errorCode") == "-100": + return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单已支付完成或已取消" + # 订单已支付完成或已取消 + if ( + pay_channel_res.get("mcashierConfirmInfo", {}).get("errorCode", "") + == "-100" + ): + return BusinessCode.JD_ORDER_EXPIRED_ERR, "订单已支付完成或已取消" + # 获取数据 + pay_info = pay_channel_res["payInfo"] + # url转码 + wx_pay_info: str = ( + f"weixin://app/wxe75a2e68877315fb/pay/?nonceStr={pay_info['nonceStr']}&package={parse.quote(pay_info['package'])}&partnerId={pay_info['partnerId']}&prepayId={pay_info['prepayId']}&timeStamp={pay_info['timeStamp']}&sign={pay_info['sign']}×tamp={pay_info['timeStamp']}" + ) + logger.info(f"获取微信app端支付参数返回:{wx_pay_info}") + return BusinessCode.SUCCESS, wx_pay_info diff --git a/apps/jd/services/ctrip.py b/apps/jd/services/ctrip.py new file mode 100644 index 0000000..d7788ad --- /dev/null +++ b/apps/jd/services/ctrip.py @@ -0,0 +1,817 @@ +import hashlib +import json +from logging import LoggerAdapter +import platform +import re +import time + +import fake_useragent +from curl_cffi import ProxySpec, requests + +from apps.jd.schemas.models import QueryCardResponseData +from apps.shared.proxy_pool.proxy_pool import ProxyPoolFactory +from core.config import ProxyPoolType +from core.exceptions import JDServiceException +from core.responses import BusinessCode +from observability.logging import get_logger_with_trace + +logger: LoggerAdapter = get_logger_with_trace(__name__) + + +class XiechengCardSpider: + """ + 携程 + 100元:10140177420168 + 200元:10148161391225 + 300元:10148163028307 + 500元:10148178960836 + 1000元:10148179392280 + """ + + def __init__(self, cookies, order_num, sku_id): + self.eid = None + self.x_token = None + self._session = requests.Session() + self._order_num = order_num + self.cookies = cookies + self.time_stamp = int(time.time() * 1000) + self.current_os = platform.system() + self.sku_id = sku_id + self.submit_order_url = "https://api.m.jd.com/appstore/submitorder" + self.ticket_url = "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkcaptcha" + self.jd_api = "https://api.m.jd.com/api" + self.action_url = "https://api.m.jd.com/client.action" + self.check_captcha_url = ( + "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkcaptcha" + ) + self._expiring_pool = ProxyPoolFactory.get_proxy_pool( + ProxyPoolType.EXPIRING, expire_time=60 + ) + self.user_agent = fake_useragent.FakeUserAgent().chrome + + def _get_proxy(self): + proxy = self._expiring_pool.get_proxy(order_id=self._order_num) + return ProxySpec(all=proxy) if proxy else None + + # def get_ticket_res(self): + # for i in range(1): + # try: + # headers = { + # "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + # "Accept-Language": "zh-CN,zh;q=0.9", + # "Cache-Control": "no-cache", + # "Connection": "keep-alive", + # "Pragma": "no-cache", + # "Upgrade-Insecure-Requests": "1", + # "User-Agent": self.user_agent, + # } + # url = f"http://119.23.175.61:8877/captcha/tx?aid=2093769752&ip={self.proxy}&host=https://t.captcha.qq.com" + # logger.info(f"获取ticket请求代理:{self.proxy}") + # logger.info(f"获取ticket请求:{url}") + # response = self._session.get( + # url, headers=headers, verify=False, timeout=60 + # ) + # logger.info(f"获取ticket结果:{response.text}") + # if response.json().get("msg_code") != 200: + # continue + # if response.json().get("result", {}).get( + # "ticket" + # ) and response.json().get("result", {}).get("randstr"): + # return { + # "ticket": response.json().get("result", {}).get("ticket"), + # "randstr": response.json().get("result", {}).get("randstr"), + # } + # except Exception as e: + # logger.error(e) + # raise Exception("获取ticket失败") + + def get_pay_res(self, order_id): + headers = { + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "content-type": "application/json;charset=UTF-8", + "origin": "https://trade.m.jd.com", + "priority": "u=1, i", + "referer": "https://trade.m.jd.com/", + "sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": self.user_agent, + "x-referer-page": "https://trade.m.jd.com/order/orderlist_jdm.shtml", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookies, + } + body = ( + '{"appType":3,"bizType":"2","deviceUUId":"","platform":3,"sceneval":"2","source":"m_inner_myJd.orderFloor_orderlist","systemBaseInfo":"{\\"pixelRatio\\":1.25,\\"screenWidth\\":2048,\\"screenHeight\\":1152,\\"windowWidth\\":414,\\"windowHeight\\":1034,\\"statusBarHeight\\":null,\\"safeArea\\":{\\"bottom\\":0,\\"height\\":0,\\"left\\":0,\\"right\\":0,\\"top\\":0,\\"width\\":0},\\"bluetoothEnabled\\":false,\\"locationEnabled\\":false,\\"wifiEnabled\\":false,\\"deviceOrientation\\":\\"landscape\\",\\"benchmarkLevel\\":-1,\\"brand\\":\\"\\",\\"model\\":\\"\\",\\"system\\":null,\\"platform\\":\\"Win32\\",\\"SDKVersion\\":\\"\\",\\"enableDebug\\":false,\\"language\\":\\"zh-CN\\",\\"version\\":\\"\\",\\"theme\\":\\"light\\",\\"fontSizeSetting\\":null,\\"albumAuthorized\\":false,\\"cameraAuthorized\\":false,\\"locationAuthorized\\":false,\\"microphoneAuthorized\\":false,\\"notificationAuthorized\\":false,\\"notificationAlertAuthorized\\":false,\\"notificationBadgeAuthorized\\":false,\\"notificationSoundAuthorized\\":false,\\"phoneCalendarAuthorized\\":false,\\"locationReducedAccuracy\\":false,\\"environment\\":\\"\\"}","orderId":"%s","origin":10,"tenantCode":"jgm","bizModelCode":"2","bizModeClientType":"M","bizModeFramework":"Taro","externalLoginType":1,"token":"3852b12f8c4d869b7ed3e2b3c68c9436","appId":"m91d27dbf599dff74"}' + % order_id + ) + encrypt_body = self.sha256_hash(body) + timestamp = f"{int(time.time() * 1000)}" + object_id = "9b070" + function_id = "pay_info_m" + h5st = self.get_h5st(timestamp, encrypt_body, object_id, function_id) + + params = { + "t": f"{self.time_stamp}", + "loginType": "2", + "loginWQBiz": "golden-trade", + "appid": "m_core", + "client": "Win32", + "clientVersion": "", + "build": "", + "osVersion": "null", + "screen": "2048*1152", + "networkType": "4g", + "partner": "", + "forcebot": "", + "d_brand": "", + "d_model": "", + "lang": "zh-CN", + "scope": "", + "sdkVersion": "", + "openudid": "", + "uuid": "17295878824571442265187", + "x-api-eid-token": self.x_token, + "functionId": "pay_info_m", + "body": body, + "h5st": h5st, + } + proxy = self._get_proxy() + response = self._session.get( + self.action_url, + headers=headers, + params=params, + verify=False, + proxies=proxy, + ) + return response.json() + + def plat_pay_channel_res(self, pay_id): + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://mpay.m.jd.com", + "priority": "u=1, i", + "referer": "https://mpay.m.jd.com/", + "sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": self.user_agent, + "x-referer-page": "https://mpay.m.jd.com/mpay.623f9498223cf9b9de9f.html", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookies, + } + params = {"functionId": "platPayChannel", "appid": "mcashier", "scval": "mpay"} + body = ( + '{"appId":"m_D1vmUq63","payId":"%s","source":"mcashier","origin":"h5","mcashierTraceId":1729842386189}' + % pay_id + ) + encrypt_body = self.sha256_hash(body) + timestamp = f"{int(time.time() * 1000)}" + object_id = "303a7" + function_id = "plat_pay_channel" + proxy = self._get_proxy() + h5st = self.get_h5st(timestamp, encrypt_body, object_id, function_id) + data = {"body": body, "x-api-eid-token": self.x_token, "h5st": h5st} + response = self._session.post( + self.action_url, + headers=headers, + params=params, + data=data, + verify=False, + proxies=proxy, + ) + return response.json() + + def get_locdetails(self, order_id): + url = "https://api.m.jd.com/" + data = { + "appid": "loc", + "loginType": "2", + "cthr": "1", + "loginWQBiz": "locdetails", + "t": f"{int(time.time() * 1000)}", + "functionId": "queryLocOrderDetail", + "body": '{"version":"1.0.0","source":"","requestId":1757908327795,"orderId":"%s","oldHttp2Color":true}' + % order_id, + } + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "cache-control": "no-cache", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://locdetails.jd.com", + "pragma": "no-cache", + "priority": "u=1, i", + "referer": "https://locdetails.jd.com/", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": self.user_agent, + "x-referer-page": "https://locdetails.jd.com/h5/index.html", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookies, + } + # proxy = self._get_proxy() + response = self._session.post( + url, + headers=headers, + data=data, + verify=False, + # proxies={ + # "http": proxy, + # "https": proxy, + # }, + ) + res = response.json() + if res.get("code") == 20001: + return BusinessCode.JD_ORDER_CK_ERR, QueryCardResponseData( + order_status="", + card_num="", + card_pwd="", + remark=str(res), + ) + + if res and res.get("code") == 0: + logger.info(f"请求结果 {response.json()}") + data = res.get("data", {}) + order_status_data = data.get("orderStatusData", {}) + shop_sku_info_list = data.get("shopSkuInfoList", []) + + card_num = "" + card_pwd = "" + + if shop_sku_info_list and len(shop_sku_info_list) > 0: + sku_infos = shop_sku_info_list[0].get("skuInfos", []) + if sku_infos and len(sku_infos) > 0: + code_info = sku_infos[0].get("codeInfo", []) + if code_info and len(code_info) > 0: + card_num = code_info[0].get("cardNum", "") + card_pwd = code_info[0].get("pwdNum", "") + + return BusinessCode.SUCCESS, QueryCardResponseData( + order_status=order_status_data.get("statusName", ""), + card_num=card_num, + card_pwd=card_pwd, + ) + else: + return BusinessCode.JD_ORDER_NORMAL_ERR, QueryCardResponseData( + order_status="", card_num="", card_pwd="", remark=str(res) + ) + + def plat_wx_pay_res(self, pay_id): + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://mpay.m.jd.com", + "priority": "u=1, i", + "referer": "https://mpay.m.jd.com/", + "sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": self.user_agent, + "x-referer-page": "https://mpay.m.jd.com/mpay.623f9498223cf9b9de9f.html", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookies, + } + params = {"functionId": "platWapWXPay", "appid": "mcashier", "scval": "mpay"} + body = ( + '{"appId":"m_D1vmUq63","payId":"%s","eid":"%s","source":"mcashier","origin":"h5","mcashierTraceId":1729837716957}' + % (pay_id, self.eid) + ) + encrypt_body = self.sha256_hash(body) + timestamp = f"{int(time.time() * 1000)}" + object_id = "303a7" + function_id = "plat_wx_pay" + h5st = self.get_h5st(timestamp, encrypt_body, object_id, function_id) + data = {"body": body, "x-api-eid-token": self.x_token, "h5st": h5st} + proxy = self._get_proxy() + response = self._session.post( + self.action_url, + headers=headers, + params=params, + data=data, + verify=False, + proxies=proxy, + ) + return response.json() + + # def get_deep_link_res(self, mweb_url): + # headers = { + # "Host": "wx.tenpay.com", + # "sec-ch-ua-platform": '"Windows"', + # "User-Agent": self.user_agent, + # "sec-ch-ua": '"Chromium";v="130", "Microsoft Edge";v="130", "Not?A_Brand";v="99"', + # "sec-ch-ua-mobile": "?0", + # "Accept": "*/*", + # "Sec-Fetch-Site": "same-origin", + # "Sec-Fetch-Mode": "cors", + # "Sec-Fetch-Dest": "empty", + # "Referer": mweb_url, + # "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + # } + # prepay_id = re.search(r"prepay_id=(wx\w+)", mweb_url).group(1) + # package = re.search(r"package=(\d+)", mweb_url).group(1) + # ticket_res = self.get_ticket_res() + # logger.info(f"订单号:{self._order_num},获取ticket返回:{ticket_res}") + # ticket = ticket_res["ticket"] + # randstr = ticket_res["randstr"] + # params = { + # "ticket": ticket, + # "randstr": randstr, + # "prepayid": prepay_id, + # "package": package, + # } + # proxy = self._get_proxy() + # response = self._session.get( + # self.check_captcha_url, + # headers=headers, + # params=params, + # verify=False, + # proxies=proxy, + # ) + # return response.json() + + @staticmethod + def sha256_hash(text): + # 创建 SHA-256 哈希对象 + sha256 = hashlib.sha256() + # 更新哈希对象(输入必须为字节类型) + sha256.update(text.encode("utf-8")) + # 返回 16 进制格式的哈希值 + return sha256.hexdigest() + + def _get_pt_pin(self, cookie_str): + cookies = {} + for item in cookie_str.strip(";").split(";"): + if "=" in item: + key, value = item.strip().split("=", 1) + cookies[key] = value + + pt_pin = cookies.get("pt_pin") + return pt_pin + + def get_h5st(self, timestamp, body, object_id, function_id): + sua = "Windows NT 10.0; Win64; x64" + pt_pin = self._get_pt_pin(self.cookies) + params_str = f"ai={object_id}&sua={sua}&pin={pt_pin}&appid=m_core&functionId={function_id}&body={body}&client=Win32&clientVersion=2.5.2&t={timestamp}" + h5st = requests.get(f"http://127.0.0.1:8887/jd/h5st?{params_str}").text + return h5st + + def get_x_token(self): + proxy = None + _proxy = self._get_proxy() + if _proxy: + proxy = _proxy.get("all") + + data = {"type": "1", "str": self.user_agent} + if proxy: + data["proxy"] = proxy + + response = requests.post( + "http://127.0.0.1:8887/api/stash/algorithm", + data=data, + ) + logger.info(f"获取x-api-eid-token返回:{response.text}") + res = response.json() + token = res["data"]["token"] + eid = res["data"]["eid"] + return token, eid + + def get_current_order(self, retry_count: int = 3): + if not retry_count: + return JDServiceException(BusinessCode.JD_ORDER_RISK_ERR) + body = ( + '{"deviceUUID":"6785593751540242498","appId":"wxae3e8056daea8727","appVersion":"2.5.2","tenantCode":"jgm","bizModelCode":"3","bizModeClientType":"M","token":"3852b12f8c4d869b7ed3e2b3c68c9436","externalLoginType":1,"referer":"https://item.m.jd.com/","resetGsd":true,"useBestCoupon":"1","locationId":"1-72-2819-0","packageStyle":true,"sceneval":"2","balanceCommonOrderForm":{"supportTransport":false,"action":1,"overseaMerge":false,"international":false,"netBuySourceType":0,"appVersion":"2.5.2","tradeShort":false},"balanceDeviceInfo":{"resolution":"2048*1152"},"cartParam":{"skuItem":{"skuId":"%s","num":"1","orderCashBack":false,"extFlag":{}}}}' + % self.sku_id + ) + encrypt_body = self.sha256_hash(body) + timestamp = f"{int(time.time() * 1000)}" + object_id = "bd265" + function_id = "balance_getCurrentOrder_m" + h5st = self.get_h5st(timestamp, encrypt_body, object_id, function_id) + headers = { + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "cache-control": "no-cache", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://trade.m.jd.com", + "pragma": "no-cache", + "priority": "u=1, i", + "referer": "https://trade.m.jd.com/", + "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": self.user_agent, + "x-referer-page": "https://trade.m.jd.com/pay", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookies, + } + + url = "https://api.m.jd.com/client.action" + data = { + "t": timestamp, + "body": body, + "h5st": h5st, + "scval": self.sku_id, + "client": "Win32", + "clientVersion": "2.5.2", + "osVersion": "other", + "screen": "2048*1152", + "networkType": "false", + "d_brand": "", + "d_model": "", + "lang": "zh-CN", + "sdkVersion": "2.5.2", + "appid": "m_core", + "openudid": "", + "x-api-eid-token": self.x_token, + "functionId": "balance_getCurrentOrder_m", + "uuid": "17573259367521124301157", + "loginType": "2", + "xAPIScval3": "unknown", + } + proxy = self._get_proxy() + response = self._session.post( + url, + headers=headers, + data=data, + impersonate="chrome", + verify=False, + proxies=proxy, + ) + if ( + response.ok + and response.json() + and response.json().get("body", {}).get("errorCode") == "601" + ): + return self.get_current_order(retry_count - 1) + + def submit_order(self): + body = { + "deviceUUID": "6279258698405299736", + "appId": "wxae3e8056daea8727", + "tenantCode": "jgm", + "bizModelCode": "3", + "bizModeClientType": "M", + "token": "3852b12f8c4d869b7ed3e2b3c68c9436", + "externalLoginType": 1, + "appVersion": "2.5.2", + "referer": "https://item.m.jd.com/", + "checkPayPassport": False, + "checkpwdV2": False, + "isEncryptionMobile": True, + "outStockVendorIdList": [12514479], + "mainSkuIdList": [int(self.sku_id)], + "balanceDataServerSkuVOList": [ + { + "id": int(self.sku_id), + "jdPrice": "499.00", + "buyNum": 1, + "firstCategoryId": 4938, + "secondCategoryId": 11760, + "thirdCategoryId": 22501, + "promoId": 305236894242, + "venderId": 12514479, + "type": 1, + } + ], + "balanceTableWareVoList": [{}], + "cashierDeskBackUrl": f"https://trade.m.jd.com/buy/done.shtml?dealId=%24%7BorderId%7D&sceneval=2&fromPay=1&ptag=7039.27.14&gift_skuid={self.sku_id}&gift_venderid=12514479&gift_cid=22501&normal=1", + "payType": "4", + "subPayType": "", + "licenseList": [], + "balanceCommonOrderForm": { + "action": 1, + "overseaMerge": False, + "international": False, + "netBuySourceType": 0, + "appVersion": "2.5.2", + "supportTransport": False, + "tradeShort": False, + "useChannelFlag": "10000000", + "hasSingleOrderGovSubsidy": False, + "unionPayCouponEffective": False, + "oldAgeStyle": False, + "balanceRefreshByAction": "1", + "supportUserPrivacy": True, + "userPrivacyChecked": True, + }, + "balanceExt": { + "baiDuJumpButtonSwitch": False, + "bubbleTips": {"enable": True, "num": 1, "time": 3, "useNum": 0}, + "cashierDeskEnable": True, + "cashierPayFlag": "0", + "checkIdInfo": False, + "couponRedText": "", + "hasBackupStorage": False, + "hasCwbg": False, + "hasFreightInsurance": False, + "isInternational": False, + "isSupportTranport": False, + "jdCombineType": 0, + "knowledgeServiceStatus": 0, + "noAddressMatchDegradeSwitch": True, + "overseaMerge": False, + "plusFloorStr": '{"addressType":0,"area":"19-1601-3633-63243","freightInfoList":[],"invoiceType":1,"payWay":4,"plusPromotionRequest":{"couponTotalAmount":0.00,"fareCouponTotalAmount":0.00,"officialDiscountTotalDiscount":0.00,"promotionReduceTotalAmount":0.00,"promotionTotalPrice":499.00,"redBagTotalAmount":0.00,"totalCashGiftDiscount":0,"totalCashGiftNew":0,"totalCashGiftOld":0,"totalParallelDiscount":0.00},"plusStatus":"203","requestConditionList":[105,106,305,529,523,521,522,351],"skuInfoRequestList":[{"col_type":"0","companyType":0,"factoryShip":0,"firstCategory":4938,"isCanUseDongCoupon":1,"isCanUseJingCoupon":1,"isJxzy":0,"isLoc":1,"isOverseaPurchase":0,"jdPrice":"499.00","number":"1","secondCategory":11760,"shopId":12204479,"skuExtension":{"fields":{"fare":"2849262","saler":"","saleAttributes":"[{\\"saleName\\":\\"颜色\\",\\"dim\\":1},{\\"saleName\\":\\"尺码\\",\\"dim\\":2}]","maxBuyNum":"19","is7ToReturn":"0","features":"consumptionVAT:0,inputVAT:0,outputVAT:0","vender_attribute":"","isHitGovSubsidy":"0","selectedGBCZGovConsumerCoupon":"false","yn":"1","vender_name":"易点生活电子商务有限公司","product_id":"10028314345641","sale_atts":"","warranty":"","timeliness_id":"0","sku_name":"【谨防刷单诈骗】沃尔玛大卖场卡500元 官方卡密 卡号8688 不支持山姆 本店不刷单 谨防诈骗 不支持退换","supply_unit":"","model":"","shopId":"12204479","cn_sku_name":"【谨防刷单诈骗】沃尔玛大卖场卡500元 官方卡密 卡号8688 不支持山姆 本店不刷单 谨防诈骗 不支持退换","height":"0","MN":"19","first_buyer_post":"4541","unLimit_cid":"22501","isCanVAT":"0","pay_first":"1","tsfw":"p8,","ms":"0","weight":"0","sku_id":"%s","tax":"consumptionVAT:0,inputVAT:0,outputVAT:0","shop_name":"沃尔玛礼品卡专卖店","product_name":"【谨防刷单诈骗】沃尔玛大卖场卡500元 官方卡密 卡号8688 不支持山姆","brand_id":"250870","shop_id":"12204479","size":"不支持退换","brandId":"250870","col_type":"0","color":"本店不刷单 谨防诈骗","cn_color":"本店不刷单 谨防诈骗","isLOC":"2","img_dfs_url":"jfs/t1/225028/24/16896/94540/6667fb55Fad484540/4069fd317318e9da.jpg","outer_id":"DSwemmck0500","platform":"1","vender_id":"12514479","jc_buyer":"","category_id":"22501","fxg":"0","isQdh":"0","venderAttribute":"","sku_tag":"0","category_id1":"4938","sku_mark":"0","template_type_attributes":"[{\\"attrid\\":\\"1001050620\\",\\"dim\\":1,\\"saleName\\":\\"颜色\\",\\"saleValue\\":\\"本店不刷单 谨防诈骗\\",\\"sequenceNo\\":1,\\"valueId\\":\\"2916524754\\"},{\\"attrid\\":\\"1001051741\\",\\"dim\\":2,\\"saleName\\":\\"尺码\\",\\"saleValue\\":\\"不支持退换\\",\\"sequenceNo\\":1,\\"valueId\\":\\"3924738280\\"}]","category_id2":"11760","sale_template_id":"POP_MODEL","jc_saler":"","day_limited_sales":"19","length":"0","locGroupId":"-100","vender_col_type":"0","sku_status":"1","allnum":"0","containsGovConsumerCoupon":"false","width":"0"},"fresh":false,"num":1,"parallelPromo":false,"samShop":false,"selfSupport":false,"shoppingMalls":false,"skuPriceAfterSinglePromotion":"499.00","skuUuid":"1012_F2t2t3H1499005262095200256","spuId":"-1","storeId":"-1","vendor":"12514479","vendorType":2},"skuId":%s,"skuMark":"","spuId":"-1","thirdCategory":22501,"uuid":"1012_F2t2t3H1499005262095200256","venderId":12514479,"vender_bizid":",popsop,","vender_col_type":"0","vendorType":0}],"totalPrice":"499.00"}' + % (self.sku_id, self.sku_id), + "selectedCouponNum": 0, + "sellLargeDay": "7", + "supportPaymentSkuList": [self.sku_id], + "useBestCoupon": True, + }, + "actualPayment": "499.00", + "sendGift": {}, + "dsList": [ + {"paramName": "report_time", "paramVal": ""}, + {"paramName": "deal_id", "paramVal": ""}, + {"paramName": "buyer_uin"}, + {"paramName": "pin", "paramVal": ""}, + { + "paramName": "cookie_pprd_p", + "paramVal": "UUID.17512753463451844059233-LOGID.1751857899901.1014192400", + }, + { + "paramName": "cookie_pprd_s", + "paramVal": "76161171.17512753463451844059233.1751275346.1751856681.1751857862.11", + }, + {"paramName": "cookie_pprd_t"}, + {"paramName": "ip", "paramVal": ""}, + {"paramName": "visitkey", "paramVal": "6279258698405299736"}, + {"paramName": "gen_entrance", "paramVal": ""}, + {"paramName": "deal_src", "paramVal": "7"}, + {"paramName": "item_type", "paramVal": "1"}, + {"paramName": "fav_unixtime", "paramVal": ""}, + {"paramName": "pay_type", "paramVal": "0"}, + {"paramName": "ab_test", "paramVal": ""}, + {"paramName": "serilize_type", "paramVal": "0"}, + {"paramName": "property1", "paramVal": "0"}, + {"paramName": "property2", "paramVal": "0"}, + {"paramName": "property3", "paramVal": "0"}, + {"paramName": "property4", "paramVal": "0"}, + {"paramName": "seller_uin", "paramVal": "0"}, + {"paramName": "pp_item_id", "paramVal": ""}, + {"paramName": "openid"}, + {"paramName": "orderprice", "paramVal": ""}, + {"paramName": "actiontype", "paramVal": ""}, + {"paramName": "extinfo", "paramVal": ""}, + {"paramName": "ext1", "paramVal": self.sku_id}, + {"paramName": "ext2", "paramVal": ""}, + {"paramName": "ext3", "paramVal": ""}, + {"paramName": "ext4", "paramVal": ""}, + {"paramName": "ext5", "paramVal": ""}, + {"paramName": "ext6"}, + {"paramName": "ext7", "paramVal": ""}, + {"paramName": "ext8", "paramVal": "0"}, + {"paramName": "ext9", "paramVal": "0|0|0|0|0||0|0"}, + {"paramName": "ext10", "paramVal": "|||"}, + { + "paramName": "ext11", + "paramVal": "http://wq.jd.com/wxapp/pages/pay/index/index", + }, + {"paramName": "ext12", "paramVal": "1"}, + {"paramName": "ext13", "paramVal": ""}, + {"paramName": "ext14", "paramVal": ""}, + {"paramName": "ext15", "paramVal": ""}, + {"paramName": "ext16", "paramVal": ""}, + { + "paramName": "ext17", + "paramVal": "76161171%7Ciosapp%7Ct_335139774%7Cappshare%7CCopyURL_shareidde4fa5e467a1fbe323b52ed0fa61777f5ce3e1fe17503293501852_shangxiang_none%7C1751857899902", + }, + {"paramName": "ext18"}, + {"paramName": "ext19", "paramVal": ""}, + {"paramName": "ext20"}, + { + "paramName": "fpa", + "paramVal": "7f07f4e1-6ac2-2918-ff9a-18320538d8b2-1743129927", + }, + { + "paramName": "fpb", + "paramVal": "BApXSBoHW4fJA8VrxmIXHrE1TcUW-_mloBgFTnghK9xJ1MvE2-4G2", + }, + {"paramName": "ext21", "paramVal": ""}, + {"paramName": "ext22", "paramVal": ""}, + {"paramName": "ext23", "paramVal": "NULL"}, + {"paramName": "ext24", "paramVal": "NULL"}, + {"paramName": "ext25", "paramVal": "NULL"}, + {"paramName": "ext26", "paramVal": "NULL"}, + {"paramName": "ext27", "paramVal": "NULL"}, + {"paramName": "ext28", "paramVal": "NULL"}, + {"paramName": "ext29", "paramVal": "NULL"}, + {"paramName": "ext30", "paramVal": "NULL"}, + {"paramName": "ext31", "paramVal": "NULL"}, + {"paramName": "ext32", "paramVal": "NULL"}, + {"paramName": "ext33", "paramVal": "NULL"}, + {"paramName": "ext34", "paramVal": "NULL"}, + {"paramName": "ext35", "paramVal": "NULL"}, + {"paramName": "ext36", "paramVal": "NULL"}, + {"paramName": "ext37", "paramVal": "NULL"}, + {"paramName": "ext38", "paramVal": "NULL"}, + {"paramName": "dt", "paramVal": ""}, + ], + "govUseGisLocation": False, + "packageStyle": True, + "sceneval": "2", + "balanceDeviceInfo": {"resolution": "2048*1152"}, + "balanceId": "5351641558031892481751857904612", + } + encrypt_body = self.sha256_hash(json.dumps(body)) + timestamp = f"{int(time.time() * 1000)}" + object_id = "cc85b" + function_id = "balance_submitOrder_m" + h5st = self.get_h5st(timestamp, encrypt_body, object_id, function_id) + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "cache-control": "no-cache", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://trade.m.jd.com", + "pragma": "no-cache", + "priority": "u=1, i", + "referer": "https://trade.m.jd.com/", + "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": self.user_agent, + "x-referer-page": "https://trade.m.jd.com/pay", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookies, + } + data = { + "scval": self.sku_id, + "body": json.dumps(body), + "t": timestamp, + "h5st": h5st, + "appid": "m_core", + "client": "Win32", + "clientVersion": "2.5.2", + "d_brand": "", + "d_model": "", + "functionId": "balance_submitOrder_m", + "lang": "zh-CN", + "loginType": "2", + "networkType": "false", + "osVersion": "", + "screen": "1152*2048", + "sdkVersion": "", + "uuid": "17573259367521124301157", + "x-api-eid-token": self.x_token, + "xAPIScval2": "wx", + "xAPIScval3": "unknown", + } + url = "https://api.m.jd.com/client.action" + proxy = self._get_proxy() + response = self._session.post( + url, + headers=headers, + data=data, + verify=False, + proxies=proxy, + ) + + logger.info( + f"订单号:{self._order_num},app_store提交订单返回:{response.json()}" + ) + return response.json() + + def run(self): + self.x_token, self.eid = self.get_x_token() + # 提交预付款订单 + self.get_current_order() + order_res = self.submit_order() + logger.info( + f"订单号:{self._order_num},app_store提交预付款订单返回:{order_res}" + ) + # 火爆 + if order_res.get("body", {}).get("errorCode") == "7201": + return BusinessCode.JD_ORDER_RISK_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(order_res), + } + # 未登录 + if order_res.get("body", {}).get("errorCode") == "302": + return BusinessCode.JD_ORDER_CK_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": order_res.get("body", {}).get("errorReason"), + } + # 无货 + if order_res.get("body", {}).get("errorCode") == "722": + return BusinessCode.JD_ORDER_STOCK_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": order_res.get("body", {}) + .get("submitOrderPromptVO", {}) + .get("title"), + } + # 火爆 + if order_res.get("body", {}).get("errorCode") == "601": + return BusinessCode.JD_ORDER_RISK_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(order_res), + } + if order_res.get("code") != "0": + return BusinessCode.JD_ORDER_NORMAL_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(order_res), + } + if order_res.get("body", {}).get("errorCode") == "601": + return BusinessCode.JD_ORDER_NORMAL_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(order_res), + } + order_id = order_res.get("body", {}).get("order", {}).get("orderId") + # 获取支付信息 + pay_res = self.get_pay_res(order_id) + logger.info(f"订单号:{self._order_num},app_store获取支付信息返回:{pay_res}") + if ( + order_res.get("body", {}).get("errorCode") == "302" + and order_res.get("body", {}).get("errorReason") == "未登录" + ): + return BusinessCode.JD_ORDER_CK_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(order_res), + } + if pay_res.get("code") != "0": + return BusinessCode.JD_ORDER_NORMAL_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(pay_res), + } + pay_id = pay_res["body"]["payId"] + # 获取微信支付信息 + return self.refresh_payment_url(pay_id, order_id) + + def refresh_payment_url(self, pay_id: str, order_id: int): + logger.info(f"订单号:{pay_id},app_store刷新支付链接 {order_id}") + pay_channel_res = self.plat_pay_channel_res(pay_id) + logger.info( + f"订单号:{self._order_num},app_store请求微信渠道返回:{pay_channel_res}" + ) + if pay_channel_res.get("code") == "601": + return BusinessCode.JD_ORDER_RISK_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(pay_channel_res), + } + if pay_channel_res.get("code") != "0": + return BusinessCode.JD_ORDER_NORMAL_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(pay_channel_res), + } + wx_pay_res = self.plat_wx_pay_res(pay_id) + logger.info( + f"订单号:{self._order_num},app_store获取微信支付信息返回:{wx_pay_res}" + ) + if wx_pay_res.get("code") != "0": + return BusinessCode.JD_ORDER_NORMAL_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(wx_pay_res), + } + if wx_pay_res.get("errorCode") == "-1": + return BusinessCode.JD_ORDER_CK_ERR, { + "deeplink": "", + "order_id": "", + "pay_id": "", + "remark": str(wx_pay_res), + } + return BusinessCode.SUCCESS, { + "deeplink": wx_pay_res.get("payInfo", {}).get("mweb_url"), + "order_id": str(order_id), + "pay_id": str(pay_id), + } diff --git a/apps/jd/services/delete.py b/apps/jd/services/delete.py new file mode 100644 index 0000000..70f6752 --- /dev/null +++ b/apps/jd/services/delete.py @@ -0,0 +1,237 @@ +import hashlib +import json +import re +import time + +import execjs +from curl_cffi import requests + +from observability.logging import get_logger_with_trace + + +logger = get_logger_with_trace(__name__) + + +class DeleteOrder: + + def __init__(self, cookie, order_id): + self.cookie = cookie + self.order_id = order_id + self.timestamp = int(time.time() * 1000) + self.h5st_ctx = self.get_js_obj("h5st4.2.js") + self.ai = "8108f" + self.version = "4.2" + self.p1 = None + self.fp = None + self.tk = None + self.mode = None + self.rd = None + self.sign = None + self.timestamp_sha256 = None + self.t_sha256 = None + self.t_send_data = None + + def get_js_obj(self, js): + with open(f"./js/{js}", "r", encoding="utf-8") as file: + js_code = file.read() + return execjs.compile(js_code, cwd="./node_modules") + + def encrypt_body(self): + key = "e7c398ffcb2d4824b4d0a703e38eb0bb" + str_to_hash = f"{self.order_id}1sx{self.timestamp}{key}" + enc_str = hashlib.md5(str_to_hash.encode("utf-8")).hexdigest() + body = { + "orderId": self.order_id, + "source": "1", + "channelSource": "sx", + "t": self.timestamp, + "encStr": enc_str, + } + return json.dumps(body) + + def cancel_order(self): + body = self.encrypt_body() + url = "https://api.m.jd.com/api" + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://recharge.m.jd.com", + "pragma": "no-cache", + "priority": "u=1, i", + "referer": "https://recharge.m.jd.com/", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", + "x-referer-page": "https://recharge.m.jd.com/orderDetail", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookie, + } + data = { + "appid": "tsw-m", + "functionId": "huafei_orderCancel", + "t": f"{self.timestamp}", + "body": body, + "client": "iPhone", + "uuid": "1735283651510338525254", + "osVersion": "16.6", + "screen": "828.0000123381615*1792.0000267028809", + "x-api-eid-token": "jdd03MOMPSVKGBFF6WCM3KNQK34LGPGSCNKB2WACDOVKFUNXQWAWDEVXSHMGQEQLJ6EUKKXZ7ARQA4CPF6EMRRUP5P7ETLEAAAAMUC37NLWQAAAAADDQSI4QCFE6GAAX", + } + response = requests.post(url, headers=headers, data=data) + print(response.text) + + def get_fp(self): + self.fp = self.h5st_ctx.call("iC") + + def get_env(self): + ctx = self.get_js_obj("h5st4.2.js") + env = ctx.call("expandParams", self.fp, self.p1) + return env + + def request_algo(self): + env = self.get_env() + url = "https://cactus.jd.com/request_algo" + params = {"g_ty": "ajax"} + timestamp_ms = int(time.time() * 1000) + data = { + "version": "4.2", + "fp": self.fp, + "appId": "8108f", + "timestamp": timestamp_ms, + "platform": "web", + "expandParams": env, + "fv": "h5_npm_v4.2.0", + } + headers = { + "accept": "application/json", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "content-type": "application/json", + "origin": "https://txsm-m.jd.com", + "pragma": "no-cache", + "referer": "https://txsm-m.jd.com/", + "sec-ch-ua": '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + } + data = json.dumps(data, separators=(",", ":")) + response = requests.post(url, headers=headers, params=params, data=data) + return response.json().get("data") + + def get_algo(self): + algo_data = self.request_algo() + print(algo_data) + algo = algo_data["result"]["algo"] + self.tk = algo_data["result"]["tk"] + self.mode = re.findall(r"algo\.(.*)\(", algo)[0] + self.rd = re.findall(r"rd='(.*)';", algo)[0] + + def get_p1(self): + self.p1 = re.findall(r"pin=(.*);", self.cookie)[0] + + def get_tm(self, timestamp): + import datetime + + # 将13位时间戳转换为datetime对象 + dt = datetime.datetime.fromtimestamp(timestamp / 1000) + year = dt.year + month = str(dt.month).zfill(2) + day = str(dt.day).zfill(2) + hour = str(dt.hour).zfill(2) + minute = str(dt.minute).zfill(2) + second = str(dt.second).zfill(2) + # 将微秒转换为毫秒 + microsecond = dt.microsecond // 1000 + return f"{year}{month}{day}{hour}{minute}{second}{microsecond}" + + def get_oe(self, ts): + oe = self.h5st_ctx.call( + "test", self.mode, self.tk, self.fp, ts, self.ai, self.rd + ) + return oe + + def get_body(self): + self.t_send_data = int(time.time() * 1000) + self.t_sha256 = int(time.time() * 1000) + self.timestamp_sha256 = self.get_tm(self.t_sha256) + ts = self.timestamp_sha256 + "74" + oe = self.get_oe(ts) + body_hex = self.h5st_ctx.call("encrypt_body", self.order_id, "delete") + t_string = f"{oe}appid:m_core&body:{body_hex}&client:Win32&clientVersion:&functionId:order_recycle_m&t:{self.t_send_data}{oe}" + self.sign = self.h5st_ctx.call("__genSign", t_string) + body = self.h5st_ctx.call("build_params", self.order_id, "delete") + return body + + def get_h5st(self): + h5st = f"{self.timestamp_sha256};{self.fp};{self.ai};{self.tk};{self.sign};{self.version};{self.t_sha256}" + return h5st + + def request_jd_recycle(self, body, h5st): + headers = { + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://trade.m.jd.com", + "pragma": "no-cache", + "referer": "https://trade.m.jd.com/", + "sec-ch-ua": '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + "x-referer-page": "https://trade.m.jd.com/order/orderlist_jdm.shtml", + "x-rp-client": "h5_1.0.0", + "cookie": self.cookie, + } + url = "https://api.m.jd.com/client.action" + data = { + "t": f"{self.t_send_data}", + "loginType": "2", + "loginWQBiz": "golden-trade", + "appid": "m_core", + "client": "Win32", + "clientVersion": "", + "build": "", + "osVersion": "null", + "screen": "1440*900", + "networkType": "4g", + "partner": "", + "forcebot": "", + "d_brand": "", + "d_model": "", + "lang": "zh-CN", + "scope": "", + "sdkVersion": "", + "openudid": "", + "uuid": "1658065822", + "x-api-eid-token": "jdd03EZ4U2HD6OVXEWMTFQVIXNMASQWBFCYSPX37R7H6QIQDPWYYPIESUHF2YNEGTGXKWHGCH5VEWPMZA4PWPPKX6ZJOKWIAAAAMOR6CEGEIAAAAADWOVM6433SKKPAX", + "functionId": "order_recycle_m", + "body": body, + "h5st": h5st, + } + response = requests.post(url, headers=headers, data=data) + print(response.text) + + def recycle_order(self): + self.get_p1() + self.get_fp() + self.get_algo() + body = self.get_body() + h5st = self.get_h5st() + self.request_jd_recycle(body=body, h5st=h5st) + + def run(self): + # 取消订单 + self.cancel_order() + # 删除订单 + self.recycle_order() diff --git a/apps/jd/services/game_area.py b/apps/jd/services/game_area.py new file mode 100644 index 0000000..81cba39 --- /dev/null +++ b/apps/jd/services/game_area.py @@ -0,0 +1,44 @@ +import time + +from curl_cffi import requests + +from observability.logging import get_logger_with_trace + +logger = get_logger_with_trace(__name__) + + +class GameArea: + + @staticmethod + def get_details(cookies, sku_id): + headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "origin": "https://recharge.m.jd.com", + "pragma": "no-cache", + "priority": "u=1, i", + "referer": "https://recharge.m.jd.com/", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1", + "x-referer-page": "https://recharge.m.jd.com/cardSettlement", + "x-rp-client": "h5_1.0.0", + "cookie": cookies, + } + params = { + "appid": "tsw-m", + "functionId": "getGameDetailBySkuId", + "t": f"{int(time.time() * 1000)}", + "body": '{"skuId":"%s","appKey":"apple","source":41}' % sku_id, + "client": "iPhone", + "uuid": "1731377463937218663686", + "osVersion": "16.6", + "screen": "1170.000046491623*2532.0001006126404", + # "h5st": "20241112145441915;l055lflh9lr1i4k8;8e94a;tk03w92d21bda18nCxAp7H5xKn6xhgPjl2DU239CUZMhS1OwR9VyMZc5hQGRyFwDwYV8pE9DyQ7wpjquSVNSz5Kj3B5v;cbe697aecf5adbdd1ee2ddb4a1943f24;4.9;1731394481915;pjbMhjpdAaYR6jkQyLlQF6Ve2roQJrJdJrESJrpjh7Jf6rJdJz1TIipjLDrgJTISJSVS6PYd1jof0bFTKqIfJqoe1rYTImFf1LofzfITJrJdJrEa-OFTGOEjLrJp-jJS5ToeyT4e6nodGSld4j4TJeYe7PYS4bVT6T1fFSYTyjpjxj5PKSEQKeFjLrJp-jJf9HIg3T0UG6VRFuWeDipjxjJOJrpjh7JjbKUXzfUSnWYQfe2XJrJdJ31QHyVT5ipjLDrgJj4f9G1WJrJdJTlPJrpjh7ZMLrJp7rJdJLYOJipjLrpjh7JjJrJdJPYOJipjLrpjh7peLDIj1XETJrpjLrJp-rojxjZe2iFjLrpjLDrg7rJdJbYOJipjLrpjh7Je2rJdJfYOJipjLrpjh7Jf_rJdJjYOJipjLrpjh7Jj2zZf9rIjLDIj6XETJrpjLrJp-rojxj5R0ipjLrpjh7pfLDIj46FjLrpjLDrg7rJdJ7FjLrpjLDrg7rJdJb1OJrpjLrJpwqJdJbFQGakNGipjLDrguqpjhjZVl6VS5C2OqmHXi_1UHCFjLDIj6rEjLrpjLD7NLDIj7qEjLrJp-jpVLf2YLfVTeqZSAGlQLT4U1nojYunjGy1QDqWRLXmXoq5dGy1QDqWRJrJdJnVO4ipjLD7N;949f348da7bbb53fcee1f3a592db0ad3", + "x-api-eid-token": "jdd03MOMPSVKGBFF6WCM3KNQK34LGPGSCNKB2WACDOVKFUNXQWAWDEVXSHMGQEQLJ6EUKKXZ7ARQA4CPF6EMRRUP5P7ETLEAAAAMTD4S5X6IAAAAADZXKZJY7RRZL4AX", + } + jd_api = "https://api.m.jd.com/api" + response = requests.get(jd_api, headers=headers, params=params) + return response.json() diff --git a/apps/jd/services/goods_apple_card.py b/apps/jd/services/goods_apple_card.py new file mode 100644 index 0000000..3499d4d --- /dev/null +++ b/apps/jd/services/goods_apple_card.py @@ -0,0 +1,145 @@ +from curl_cffi import requests + +from apps.jd.services.app_store import AppStoreSpider +from observability.logging import get_logger_with_trace + +logger = get_logger_with_trace(__name__) + + +class GoodsAppleCard(AppStoreSpider): + + def __init__( + self, + cookies, + order_num, + brand_id, + face_price, + sku_id, + username, + game_srv=None, + game_area=None, + recharge_type=1, + ): + super(GoodsAppleCard, self).__init__(cookies, order_num, face_price=face_price) + self.sku_id = sku_id + self.brand_id = brand_id + self.username = username + self.game_srv = game_srv + self.game_area = game_area + self.face_price = face_price + self.recharge_type = recharge_type + + def encrypt_username(self, username): + key = "2E1ZMAF88CCE5EBE551FR3E9AA6FF322" + username = self.js.call("encryptDes", username, key) + return username + + def submit_gp_order(self): + headers = { + "Host": "api.m.jd.com", + "pragma": "no-cache", + "cache-control": "no-cache", + "user-agent": self.__user_client, + "accept": "application/json, text/plain, */*", + "x-referer-page": "https://recharge.m.jd.com/cardSettlement", + "content-type": "application/x-www-form-urlencoded", + "x-rp-client": "h5_1.0.0", + "origin": "https://recharge.m.jd.com", + "sec-fetch-site": "same-site", + "sec-fetch-mode": "cors", + "sec-fetch-dest": "empty", + "referer": "https://recharge.m.jd.com/", + "accept-language": "zh-CN,zh;q=0.9", + "priority": "u=1, i", + "cookie": self.cookies, + } + username = self.encrypt_username(self.username) + data = {} + if self.recharge_type == 1: + data = { + "appid": "tsw-m", + "functionId": "submitGPOrder", + "t": f"{self.time_stamp}", + "body": '{"skuId":"%s","brandId":"%s","type":2,"buyNum":1,"payMode":"0","totalPrice":"%s","username":"%s","appKey":"apple","source":41,"version":"1.10","orderSource":41}' + % (self.sku_id, self.brand_id, self.face_price, username), + "client": "iPhone", + "uuid": "1731377463937218663686", + "osVersion": "16.6", + "screen": "1170.000046491623*2532.0001006126404", + # "h5st": "20241112141707038;l055lflh9lr1i4k8;8e94a;tk03w92d21bda18nCxAp7H5xKn6xhgPjl2DU239CUZMhS1OwR9VyMZc5hQGRyFwDwYV8pE9DyQ7wpjquSVNSz5Kj3B5v;8effdf684f396d5889d2bde7e769f965;4.9;1731392227038;pjbMhjpdAaYR6jkQyLlQF6Ve2roQJrJdJrESJrpjh7Jf6rJdJz1TIipjLDrgJTISJSVS6PYd1jof0bFTKqIfJqoe1rYTImFf1LofzfITJrJdJrEa-OFTGOEjLrJp-jJS5ToeyT4e6nodGSld4j4TJeYe7PYS4bVT6T1fFSYTyjpjxj5PKSEQKeFjLrJp-jJf9HIg3T0UG6VRFuWeDipjxjJOJrpjh7JjxOYRhiFPyK3Z2f2XJrJdJ31QHyVT5ipjLDrgJj4f9G1WJrJdJTlPJrpjh7ZMLrJp7rJdJLYOJipjLrpjh7JjJrJdJPYOJipjLrpjh7ZeLDIj1XETJrpjLrJp-rojxjZe2iFjLrpjLDrg7rJdJbYOJipjLrpjh75e2rJdJfYOJipjLrpjh7Jf_rJdJjYOJipjLrpjh7Jj2zZf9rIjLDIj6XETJrpjLrJp-rojxj5R0ipjLrpjh7pfLDIj46FjLrpjLDrg7rJdJ7FjLrpjLDrg7rJdJb1OJrpjLrJpwqJdJbFQGakNGipjLDrguqpjhjZVl6VS5C2OqmHXi_1UHCFjLDIj6rEjLrpjLD7NLDIj7qEjLrJp-jpVLf2YLfVTeqZSAGlQLT4U1nojYunjGy1QDqWRLXmXoq5dGy1QDqWRJrJdJnVO4ipjLD7N;204790ef3f0380d87102b52131fd50d7", + "x-api-eid-token": "jdd03MOMPSVKGBFF6WCM3KNQK34LGPGSCNKB2WACDOVKFUNXQWAWDEVXSHMGQEQLJ6EUKKXZ7ARQA4CPF6EMRRUP5P7ETLEAAAAMTDYRX6SIAAAAACB66T3PYQZLCEEX", + } + if self.recharge_type == 2: + data = { + "appid": "tsw-m", + "functionId": "submitGPOrder", + "t": f"{self.time_stamp}", + "body": '{"skuId":"%s","brandId":"%s","type":2,"buyNum":1,"payMode":"0","totalPrice":"%s","gamesrv":"%s","gamearea":"%s","username":"%s","appKey":"apple","source":41,"version":"1.10","orderSource":41}' + % ( + self.sku_id, + self.brand_id, + self.face_price, + self.game_srv, + self.game_area, + username, + ), + "client": "iPhone", + "uuid": "1731377463937218663686", + "osVersion": "16.6", + "screen": "1170.000046491623*2532.0001006126404", + # "h5st": "20241112172954252;l055lflh9lr1i4k8;8e94a;tk03w92d21bda18nCxAp7H5xKn6xhgPjl2DU239CUZMhS1OwR9VyMZc5hQGRyFwDwYV8pE9DyQ7wpjquSVNSz5Kj3B5v;dcf2dab0edd6fadd62550cc4ca5e28c4;4.9;1731403794252;pjbMhjpdAaYR6jkQyLlQF6Ve2roQJrJdJrESJrpjh7Jf6rJdJz1TIipjLDrgJTISJSVS6PYd1jof0bFTKqIfJqoe1rYTImFf1LofzfITJrJdJrEa-OFTGOEjLrJp-jJS5ToeyT4e6nodGSld4j4TJeYe7PYS4bVT6T1fFSYTyjpjxj5PKSEQKeFjLrJp-jJf9HIg3T0UG6VRFuWeDipjxjJOJrpjh7JjWW1f5LnageHNqSEOJrJdJ31QHyVT5ipjLDrgJj4f9G1WJrJdJTlPJrpjh7ZMLrJp7rJdJLYOJipjLrpjh7JjJrJdJPYOJipjLrpjh7ZeLDIj1XETJrpjLrJp-rojxjZe2iFjLrpjLDrg7rJdJbYOJipjLrpjh75e2rJdJfYOJipjLrpjh7Jf_rJdJjYOJipjLrpjh7Jj2zZf9rIjLDIj6XETJrpjLrJp-rojxj5R0ipjLrpjh7pfLDIj46FjLrpjLDrg7rJdJ7FjLrpjLDrg7rJdJb1OJrpjLrJpwqJdJbFQGakNGipjLDrguqpjhjZVl6VS5C2OqmHXi_1UHCFjLDIj6rEjLrpjLD7NLDIj7qEjLrJp-jpVLf2YLfVTeqZSAGlQLT4U1nojYunjGy1QDqWRLXmXoq5dGy1QDqWRJrJdJnVO4ipjLD7N;651ee3204b43d0d514092fa61f5629f1", + "x-api-eid-token": "jdd03MOMPSVKGBFF6WCM3KNQK34LGPGSCNKB2WACDOVKFUNXQWAWDEVXSHMGQEQLJ6EUKKXZ7ARQA4CPF6EMRRUP5P7ETLEAAAAMTD6ZVZVQAAAAADSCNVUR4IR7W54X", + } + response = requests.post(self.jd_api, headers=headers, data=data) + return response.json() + + def run(self): + # 提交预付款订单 + gp_order_res = self.submit_gp_order() + # gp_order_res = {'result': {'orderId': 304636667875, 'paySuccessUrl': ''}, 'code': '0'} + logger.info( + f"订单号:{self.order_num},商品下单提交预付款订单返回:{gp_order_res}" + ) + if gp_order_res.get("code") != "0": + return 110, gp_order_res + order_id = gp_order_res["result"]["orderId"] + + # 获取支付信息 + pay_res_ = self.get_pay_res(order_id) + logger.info(f"订单号:{self.order_num},商品下单获取支付信息返回:{pay_res_}") + if pay_res_.get("code") != "0": + return 110, pay_res_ + pay_id = pay_res_["body"]["payId"] + + # 获取微信支付信息 + pay_channel_res = self.plat_pay_channel_res(pay_id) + logger.info( + f"订单号:{self.order_num},商品下单请求微信渠道返回:{pay_channel_res}" + ) + if pay_channel_res.get("code") != "0": + return 110, pay_channel_res + wx_pay_res = self.plat_wx_pay_res(pay_id) + logger.info( + f"订单号:{self.order_num},商品下单获取微信支付信息返回:{wx_pay_res}" + ) + if wx_pay_res.get("code") != "0": + return 110, wx_pay_res + if wx_pay_res.get("errorCode") == "-1": + wx_pay_res["order_id"] = order_id + wx_pay_res["pay_id"] = pay_id + wx_pay_res["face_price"] = self.face_price + return 110, wx_pay_res + mweb_url = wx_pay_res["payInfo"]["mweb_url"] + # 获取支付链接信息 + deep_link_res = self.get_deep_link_res(mweb_url) + logger.info( + f"订单号:{self.order_num},商品下单获取支付链接deep_link信息返回:{deep_link_res}" + ) + if deep_link_res.get("retcode") != 1: + return 110, deep_link_res + return 100, { + "deeplink": deep_link_res["deeplink"], + "order_id": pay_channel_res["orderId"], + "pay_id": pay_id, + "face_price": self.face_price, + } diff --git a/apps/jd/services/jstk.py b/apps/jd/services/jstk.py new file mode 100644 index 0000000..0eb0769 --- /dev/null +++ b/apps/jd/services/jstk.py @@ -0,0 +1,690 @@ +import hashlib +import json +import random +import re +import time +import urllib +from abc import ABC, abstractmethod +from urllib.parse import urlparse + +from curl_cffi import requests + + +class JsTkBase(ABC): + def __init__(self): + self._eid = "" + self._token = "" + + def _get_sign(self, text: str) -> str: + text_bytes = text.encode("utf-8") + md5_str = hashlib.md5(text_bytes).hexdigest() + return md5_str + + def get_token(self): + return self._token + + def get_eid(self): + return self._eid + + def _td_encrypt(self, e): + """ + 将输入 e 按照给定的JS逻辑进行 JSON化 -> URL编码 -> 自定义Base64编码 -> 加斜杠 + + Args: + e: 输入的数据 (可以是字典、列表、字符串、数字等,会被JSON序列化) + + Returns: + str: 编码后的字符串,末尾带 "/" + """ + # 1. 自定义字母表,对应 Base64 的 64 个字符 + # JS中的 'u' 是 0-63 的索引对应的字符 + custom_alphabet = ( + "23IL> 2) & 63 + + # 索引 2: byte1 的后 2 位 + byte2 的前 4 位 + idx2 = (byte1 & 3) << 4 + if byte2 is not None: + idx2 |= (byte2 >> 4) & 15 + # else: 如果 byte2 不存在,这里只使用了 byte1 的后 2 位,高位补 0 + + # 索引 3: byte2 的后 4 位 + byte3 的前 2 位 + idx3 = 64 # 默认填充 + if byte2 is not None: + idx3 = (byte2 & 15) << 2 + if byte3 is not None: + idx3 |= (byte3 >> 6) & 3 + # else: 如果 byte3 不存在,只使用了 byte2 的后 4 位,高位补 0 + # else: 如果 byte2 不存在,保持填充状态 64 + + # 索引 4: byte3 的后 6 位 + idx4 = 64 # 默认填充 + if byte3 is not None: + idx4 = byte3 & 63 + # else: 如果 byte3 不存在,保持填充状态 64 + + # 根据索引查找字符或使用填充符 + encoded_string += custom_alphabet[idx1] + encoded_string += custom_alphabet[idx2] + + if idx3 == 64: + encoded_string += padding_char + else: + encoded_string += custom_alphabet[idx3] + + if idx4 == 64: + encoded_string += padding_char + else: + encoded_string += custom_alphabet[idx4] + + # 5. 在末尾添加 "/" + return encoded_string + "/" + + def _request(self, headers: dict, data: dict): + response = requests.post( + "https://jra.jd.com/jsTk.do", headers=headers, data=data + ) + result = response.json() + self._token = result.get("data", {}).get("token") + self._eid = result.get("data", {}).get("eid") + + @abstractmethod + def generate_token(self, user_agent: str, cookie: str = "") -> str: + pass + + +class NormalJsTk(JsTkBase): + def __init__(self): + super().__init__() + self.__biz_id = "gold_m" + self.__o = "m.jd.com/" + + def generate_token(self, user_agent: str, cookie: str = ""): + ctime = int(time.time() * 1000) + app_version = user_agent.replace("Mozilla/", "") + + d_param = { + "ts": { + "deviceTime": ctime, + "deviceEndTime": ctime + random.randint(50, 150), + }, + "ca": { + "tdHash": "af7c21fe564a8cf177fcc77ce8847eb0", + "contextName": "webgl,experimental-webgl", + "webglversion": "WebGL 1.0 (OpenGL ES 2.0 Chromium)", + "shadingLV": "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)", + "vendor": "WebKit", + "renderer": "WebKit WebGL", + "extensions": [ + "ANGLE_instanced_arrays", + "EXT_blend_minmax", + "EXT_clip_control", + "EXT_color_buffer_half_float", + "EXT_depth_clamp", + "EXT_disjoint_timer_query", + "EXT_float_blend", + "EXT_frag_depth", + "EXT_polygon_offset_clamp", + "EXT_shader_texture_lod", + "EXT_texture_compression_bptc", + "EXT_texture_compression_rgtc", + "EXT_texture_filter_anisotropic", + "EXT_texture_mirror_clamp_to_edge", + "EXT_sRGB", + "KHR_parallel_shader_compile", + "OES_element_index_uint", + "OES_fbo_render_mipmap", + "OES_standard_derivatives", + "OES_texture_float", + "OES_texture_float_linear", + "OES_texture_half_float", + "OES_texture_half_float_linear", + "OES_vertex_array_object", + "WEBGL_blend_func_extended", + "WEBGL_color_buffer_float", + "WEBGL_compressed_texture_astc", + "WEBGL_compressed_texture_etc", + "WEBGL_compressed_texture_etc1", + "WEBGL_compressed_texture_pvrtc", + "WEBGL_compressed_texture_s3tc", + "WEBGL_compressed_texture_s3tc_srgb", + "WEBGL_debug_renderer_info", + "WEBGL_debug_shaders", + "WEBGL_depth_texture", + "WEBGL_draw_buffers", + "WEBGL_lose_context", + "WEBGL_multi_draw", + "WEBGL_polygon_mode", + ], + "wuv": "Google Inc. (Apple)", + "wur": "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)", + }, + "m": {"compatMode": "CSS1Compat"}, + "n": { + "vendorSub": "", + "productSub": "20030107", + "vendor": "Google Inc.", + "maxTouchPoints": 0, + "doNotTrack": "1", + "pdfViewerEnabled": True, + "hardwareConcurrency": 10, + "cookieEnabled": True, + "appCodeName": "Mozilla", + "appName": "Netscape", + "appVersion": app_version, + "platform": "MacIntel", + "product": "Gecko", + "userAgent": user_agent, + "language": "zh-CN", + "onLine": True, + "webdriver": False, + "javaEnabled": False, + "deprecatedRunAdAuctionEnforcesKAnonymity": False, + "deviceMemory": 8, + "enumerationOrder": [ + "vendorSub", + "productSub", + "vendor", + "maxTouchPoints", + "scheduling", + "userActivation", + "doNotTrack", + "geolocation", + "connection", + "plugins", + "mimeTypes", + "pdfViewerEnabled", + "webkitTemporaryStorage", + "webkitPersistentStorage", + "windowControlsOverlay", + "hardwareConcurrency", + "cookieEnabled", + "appCodeName", + "appName", + "appVersion", + "platform", + "product", + "userAgent", + "language", + "languages", + "onLine", + "webdriver", + "getGamepads", + "javaEnabled", + "sendBeacon", + "vibrate", + "deprecatedRunAdAuctionEnforcesKAnonymity", + "protectedAudience", + "bluetooth", + "storageBuckets", + "clipboard", + "credentials", + "keyboard", + "managed", + "mediaDevices", + "storage", + "serviceWorker", + "virtualKeyboard", + "wakeLock", + "deviceMemory", + "userAgentData", + "login", + "ink", + "mediaCapabilities", + "devicePosture", + "hid", + "locks", + "gpu", + "mediaSession", + "permissions", + "presentation", + "serial", + "usb", + "xr", + "adAuctionComponents", + "runAdAuction", + "canLoadAdAuctionFencedFrame", + "canShare", + "share", + "clearAppBadge", + "getBattery", + "getUserMedia", + "requestMIDIAccess", + "requestMediaKeySystemAccess", + "setAppBadge", + "webkitGetUserMedia", + "clearOriginJoinedAdInterestGroups", + "createAuctionNonce", + "joinAdInterestGroup", + "leaveAdInterestGroup", + "updateAdInterestGroups", + "deprecatedReplaceInURN", + "deprecatedURNToURL", + "getInstalledRelatedApps", + "getInterestGroupAdAuctionData", + "registerProtocolHandler", + "unregisterProtocolHandler", + ], + }, + "p": [ + {"name": "PDF Viewer"}, + {"name": "Chrome PDF Viewer"}, + {"name": "Chromium PDF Viewer"}, + {"name": "Microsoft Edge PDF Viewer"}, + {"name": "WebKit built-in PDF"}, + ], + "w": {"devicePixelRatio": 2, "screenTop": 0, "screenLeft": 0}, + "s": { + "availHeight": 1080, + "availWidth": 1920, + "colorDepth": 30, + "height": 1080, + "width": 1920, + "pixelDepth": 30, + }, + "sc": {}, + "ss": { + "cookie": True, + "localStorage": True, + "sessionStorage": True, + "globalStorage": False, + "indexedDB": True, + }, + "tz": -480, + "lil": "", + "wil": "", + "wi": { + "ow": 1920, + "oh": 1080, + "iw": 274, + "ih": 959, + "etn": "[object External]", + }, + } + + headers = { + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "content-type": "application/x-www-form-urlencoded;charset=UTF-8", + "origin": "https://shop.m.jd.com", + "pragma": "no-cache", + "priority": "u=1, i", + "referer": "https://shop.m.jd.com/", + "sec-ch-ua-mobile": "?1", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": user_agent, + } + + data = { + "d": self._td_encrypt(d_param), + } + + a_param = { + "pin": "", + "oid": "", + "bizId": self.__biz_id, + "fc": self._eid if self._eid else "", + "mode": "strict", + "p": "s", + "fp": "6d2c8c60af349b1a87ddd3b065194ca4", + "ctype": "1", + "v": "4.2.8.0", + "pv": "02_mt_5LXK_60653970589", + "f": "3", + "s": self._get_sign(data["d"] + "_*_UYBN6YGTNO6DHPVB"), + "o": self.__o, + "qs": "", + "jsTk": "", + "qi": "", + } + data["a"] = self._td_encrypt(a_param) + response = self._request(headers, data) + print(response) + + +class TxSMJsTk(JsTkBase): + def __init__(self): + super().__init__() + self.__url = "https://txsm-m.jd.com/?babelChannel=ttt35" + self.__biz_id = "jdtxsm" + self.__jd_risk_token_id = "" + self.__jd_jr_td_risk_pin = "" + self.__o = "txsm-m.jd.com/" + + def get_jd_risk_token_id(self): + return self.__jd_risk_token_id + + def get_jd_jr_td_risk_pin(self): + return self.__jd_jr_td_risk_pin + + def generate_risk_pin(self, user_agent: str, cookie: str): + headers = { + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9", + "cookie": cookie, + "user-agent": user_agent, + } + response = requests.get("https://gia.jd.com/m.html", headers=headers) + text = response.text.replace(" ", "") + token = re.findall(r"jd_risk_token_id='(.*?)';", text) + if token: + self.__jd_risk_token_id = token[0] + # 正则表达式解析后面 + token = re.findall(r"jd_jr_td_risk_pin='(.*?)';", text) + if token: + self.__jd_jr_td_risk_pin = token[0] + + def generate_token(self, user_agent: str, cookie: str = ""): + self.generate_risk_pin(user_agent, cookie) + ctime = int(time.time() * 1000) + app_version = user_agent.replace("Mozilla/", "") + + d_param = { + "ts": { + "deviceTime": ctime, + "deviceEndTime": ctime + random.randint(50, 150), + }, + "ca": { + "tdHash": "af7c21fe564a8cf177fcc77ce8847eb0", + "contextName": "webgl,experimental-webgl", + "webglversion": "WebGL 1.0 (OpenGL ES 2.0 Chromium)", + "shadingLV": "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)", + "vendor": "WebKit", + "renderer": "WebKit WebGL", + "extensions": [ + "ANGLE_instanced_arrays", + "EXT_blend_minmax", + "EXT_clip_control", + "EXT_color_buffer_half_float", + "EXT_depth_clamp", + "EXT_disjoint_timer_query", + "EXT_float_blend", + "EXT_frag_depth", + "EXT_polygon_offset_clamp", + "EXT_shader_texture_lod", + "EXT_texture_compression_bptc", + "EXT_texture_compression_rgtc", + "EXT_texture_filter_anisotropic", + "EXT_texture_mirror_clamp_to_edge", + "EXT_sRGB", + "KHR_parallel_shader_compile", + "OES_element_index_uint", + "OES_fbo_render_mipmap", + "OES_standard_derivatives", + "OES_texture_float", + "OES_texture_float_linear", + "OES_texture_half_float", + "OES_texture_half_float_linear", + "OES_vertex_array_object", + "WEBGL_blend_func_extended", + "WEBGL_color_buffer_float", + "WEBGL_compressed_texture_astc", + "WEBGL_compressed_texture_etc", + "WEBGL_compressed_texture_etc1", + "WEBGL_compressed_texture_pvrtc", + "WEBGL_compressed_texture_s3tc", + "WEBGL_compressed_texture_s3tc_srgb", + "WEBGL_debug_renderer_info", + "WEBGL_debug_shaders", + "WEBGL_depth_texture", + "WEBGL_draw_buffers", + "WEBGL_lose_context", + "WEBGL_multi_draw", + "WEBGL_polygon_mode", + ], + "wuv": "Google Inc. (Apple)", + "wur": "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)", + }, + "m": {"compatMode": "CSS1Compat"}, + "n": { + "vendorSub": "", + "productSub": "20030107", + "vendor": "Google Inc.", + "maxTouchPoints": 0, + "doNotTrack": "1", + "pdfViewerEnabled": True, + "hardwareConcurrency": 10, + "cookieEnabled": True, + "appCodeName": "Mozilla", + "appName": "Netscape", + "appVersion": app_version, + "platform": "MacIntel", + "product": "Gecko", + "userAgent": user_agent, + "language": "zh-CN", + "onLine": True, + "webdriver": False, + "javaEnabled": False, + "deprecatedRunAdAuctionEnforcesKAnonymity": False, + "deviceMemory": 8, + "enumerationOrder": [ + "vendorSub", + "productSub", + "vendor", + "maxTouchPoints", + "scheduling", + "userActivation", + "doNotTrack", + "geolocation", + "connection", + "plugins", + "mimeTypes", + "pdfViewerEnabled", + "webkitTemporaryStorage", + "webkitPersistentStorage", + "windowControlsOverlay", + "hardwareConcurrency", + "cookieEnabled", + "appCodeName", + "appName", + "appVersion", + "platform", + "product", + "userAgent", + "language", + "languages", + "onLine", + "webdriver", + "getGamepads", + "javaEnabled", + "sendBeacon", + "vibrate", + "deprecatedRunAdAuctionEnforcesKAnonymity", + "protectedAudience", + "bluetooth", + "storageBuckets", + "clipboard", + "credentials", + "keyboard", + "managed", + "mediaDevices", + "storage", + "serviceWorker", + "virtualKeyboard", + "wakeLock", + "deviceMemory", + "userAgentData", + "login", + "ink", + "mediaCapabilities", + "devicePosture", + "hid", + "locks", + "gpu", + "mediaSession", + "permissions", + "presentation", + "serial", + "usb", + "xr", + "adAuctionComponents", + "runAdAuction", + "canLoadAdAuctionFencedFrame", + "canShare", + "share", + "clearAppBadge", + "getBattery", + "getUserMedia", + "requestMIDIAccess", + "requestMediaKeySystemAccess", + "setAppBadge", + "webkitGetUserMedia", + "clearOriginJoinedAdInterestGroups", + "createAuctionNonce", + "joinAdInterestGroup", + "leaveAdInterestGroup", + "updateAdInterestGroups", + "deprecatedReplaceInURN", + "deprecatedURNToURL", + "getInstalledRelatedApps", + "getInterestGroupAdAuctionData", + "registerProtocolHandler", + "unregisterProtocolHandler", + ], + }, + "p": [ + {"name": "PDF Viewer"}, + {"name": "Chrome PDF Viewer"}, + {"name": "Chromium PDF Viewer"}, + {"name": "Microsoft Edge PDF Viewer"}, + {"name": "WebKit built-in PDF"}, + ], + "w": {"devicePixelRatio": 2, "screenTop": 0, "screenLeft": 0}, + "s": { + "availHeight": 1080, + "availWidth": 1920, + "colorDepth": 30, + "height": 1080, + "width": 1920, + "pixelDepth": 30, + }, + "sc": { + "ActiveBorder": "rgb(0, 0, 0)", + "ActiveCaption": "rgb(0, 0, 0)", + "AppWorkspace": "rgb(255, 255, 255)", + "Background": "rgb(255, 255, 255)", + "ButtonFace": "rgb(239, 239, 239)", + "ButtonHighlight": "rgb(239, 239, 239)", + "ButtonShadow": "rgb(239, 239, 239)", + "ButtonText": "rgb(0, 0, 0)", + "CaptionText": "rgb(0, 0, 0)", + "GrayText": "rgb(128, 128, 128)", + "Highlight": "rgba(128, 188, 254, 0.6)", + "HighlightText": "rgb(0, 0, 0)", + "InactiveBorder": "rgb(0, 0, 0)", + "InactiveCaption": "rgb(255, 255, 255)", + "InactiveCaptionText": "rgb(128, 128, 128)", + "InfoBackground": "rgb(255, 255, 255)", + "InfoText": "rgb(0, 0, 0)", + "Menu": "rgb(255, 255, 255)", + "MenuText": "rgb(0, 0, 0)", + "Scrollbar": "rgb(255, 255, 255)", + "ThreeDDarkShadow": "rgb(0, 0, 0)", + "ThreeDFace": "rgb(239, 239, 239)", + "ThreeDHighlight": "rgb(0, 0, 0)", + "ThreeDLightShadow": "rgb(0, 0, 0)", + "ThreeDShadow": "rgb(0, 0, 0)", + "Window": "rgb(255, 255, 255)", + "WindowFrame": "rgb(0, 0, 0)", + "WindowText": "rgb(0, 0, 0)", + }, + "ss": { + "cookie": True, + "localStorage": True, + "sessionStorage": True, + "globalStorage": False, + "indexedDB": True, + }, + "tz": -480, + "lil": "", + "wil": "", + "wi": { + "ow": 1920, + "oh": 1080, + "iw": 397, + "ih": 959, + "etn": "[object External]", + }, + } + + headers = { + "accept": "*/*", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "content-type": "application/x-www-form-urlencoded;charset=UTF-8", + "origin": "https://shop.m.jd.com", + "pragma": "no-cache", + "priority": "u=1, i", + "referer": "https://shop.m.jd.com/", + "sec-ch-ua-mobile": "?1", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": user_agent, + } + + data = { + "d": self._td_encrypt(d_param), + } + + if not self.__jd_jr_td_risk_pin and self._token: + self.__jd_jr_td_risk_pin = self._token + + a_param = { + "pin": self.__jd_jr_td_risk_pin if self.__jd_jr_td_risk_pin else "", + "oid": "", + "bizId": self.__biz_id, + "fc": self._eid if self._eid else "", + "mode": "strict", + "p": "s", + "fp": "6d2c8c60af349b1a87ddd3b065194ca4", + "ctype": "1", + "v": "4.2.8.0", + "pv": "02_mt_5LXK_60653970589", + "f": "3", + "s": self._get_sign(data["d"] + "_*_UYBN6YGTNO6DHPVB"), + "o": self.__o, + "qs": urlparse(self.__url).query, + "jsTk": "", + "qi": "", + } + data["a"] = self._td_encrypt(a_param) + self._request(headers, data) diff --git a/apps/jd/services/login.py b/apps/jd/services/login.py new file mode 100644 index 0000000..89d9896 --- /dev/null +++ b/apps/jd/services/login.py @@ -0,0 +1,329 @@ +import base64 +import json +import os +import platform + +import ddddocr +import execjs +from curl_cffi import requests + +from observability.logging import get_logger_with_trace + +logger = get_logger_with_trace(__name__) + + +class LoginSpider: + + def __init__(self, phone_): + self.phone = phone_ + self.headers = { + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "content-type": "application/x-www-form-urlencoded", + "origin": "https://plogin.m.jd.com", + "pragma": "no-cache", + "priority": "u=1, i", + "referer": "https://plogin.m.jd.com/login/login?appid=300&returnurl=https%3A%2F%2Fm.jd.com%2F&source=wq_passport", + "sec-ch-ua": '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0", + } + self.cookies = {} + self.current_os = platform.system() + self.js_path = None + self.node_modules_path = None + self.load_directory() + self.slide_ctw = self.init_verify_data_js() + self.user_ctw = self.init_user_encrypt_js() + self.h5st_ctw = self.init_h5st_js() + + self.session = requests.Session() + self.ocr = ddddocr.DdddOcr() + + self.eid = "" + self.fp = "26114226dc0c6ee7f1eaf3d4abb30cf2" + self.s_token = None + self.rsa_modulus = None + self.api_st = None + self.api_fp = None + self.img1 = None + self.img2 = None + self.vt = None + self.img = None + self.jd_risk_token_id = None + self.jcap_sid = None + self.ck = None + + def load_directory(self): + if self.current_os == "Linux": + self.js_path = r"/app/js" + self.node_modules_path = r"/app/node_modules" + else: + self.js_path = "js" + self.node_modules_path = "./node_modules" + + def do_execjs(self, path): + return execjs.compile( + open(path, encoding="utf8").read(), cwd=self.node_modules_path + ) + + def init_verify_data_js(self): + js_path = os.path.join(self.js_path, "modules.js") + return execjs.compile( + open(js_path, encoding="gbk", errors="ignore").read(), + cwd=self.node_modules_path, + ) + + def init_user_encrypt_js(self): + js_path = os.path.join(self.js_path, "user_encrypt.js") + return execjs.compile( + open(js_path, encoding="gbk", errors="ignore").read(), + cwd=self.node_modules_path, + ) + + def init_h5st_js(self): + js_path = os.path.join(self.js_path, "h5st-5.0.js") + return execjs.compile( + open(js_path, encoding="gbk", errors="ignore").read(), + cwd=self.node_modules_path, + ) + + def request_jd_risk_token_id(self): + response = self.session.get( + "https://payrisk.jd.com/m.html", cookies=self.cookies, headers=self.headers + ) + return response.text + + def build_jcapsid_data(self): + return self.user_ctw.call( + "init", self.phone, self.fp, self.jd_risk_token_id, self.s_token + ) + + def request_jcapsid(self): + url = "https://plogin.m.jd.com/cgi-bin/mm/jcapsid" + data = self.build_jcapsid_data() + response = self.session.post(url, headers=self.headers, data=data) + print(f"request_jcapsid返回:{response.text}") + return response.json() + + def request_new_login_entrance(self): + url = "https://plogin.m.jd.com/cgi-bin/mm/new_login_entrance" + params = { + "lang": "chs", + "returnurl": "https://my.m.jd.com/", + "risk_jd\\[eid\\]": self.eid, + "risk_jd\\[fp\\]": self.fp, + } + response = self.session.get( + url, headers=self.headers, cookies=self.cookies, params=params + ) + print(response.text) + return response.json() + + def get_jcap_sid(self): + jcapsid_res = self.request_jcapsid() + self.jcap_sid = jcapsid_res["jcap_sid"] + + def get_login_entrance(self): + login_entrance_res = self.request_new_login_entrance() + self.s_token = login_entrance_res["s_token"] + self.rsa_modulus = login_entrance_res["rsa_modulus"] + + def build_fp_data(self): + return self.slide_ctw.call("getFp", self.phone, self.jcap_sid) + + def request_data_fp(self): + data = self.build_fp_data() + url = "https://jcap.m.jd.com/cgi-bin/api/fp" + response = self.session.post(url, headers=self.headers, data=data) + print(response.text) + return response.json() + + def get_api_params(self): + fp_res = self.request_data_fp() + self.api_st = fp_res["st"] + self.api_fp = fp_res["fp"] + + def build_captcha_data(self): + return self.slide_ctw.call("getImage", self.phone, self.jcap_sid, self.api_st) + + def request_captcha(self): + url = "https://jcap.m.jd.com/cgi-bin/api/check" + data = self.build_captcha_data() + response = requests.post(url, headers=self.headers, data=data) + print(response.text) + return response.json() + + def set_img(self, captcha_res): + self.img = captcha_res["img"] + img_json = json.loads(captcha_res["img"]) + self.api_st = captcha_res["st"] + self.img1 = img_json["b1"] + self.img2 = img_json["b2"] + + def get_captcha(self): + captcha_res = self.request_captcha() + if captcha_res.get("vt"): + self.vt = captcha_res["vt"] + self.api_st = captcha_res["st"] + return True + self.set_img(captcha_res) + return False + + def verify_captcha(self): + bg = self.img1.replace("data:image/jpg;base64,", "") + background = base64.b64decode(bg) + fg = self.img2.replace("data:image/png;base64,", "") + target = base64.b64decode(fg) + result = self.ocr.slide_match(target, background, simple_target=True) + x = round(result["target"][0] * (290 / 275)) + url = "https://jcap.m.jd.com/cgi-bin/api/check" + data = self.slide_ctw.call( + "verify", self.phone, self.jcap_sid, self.img, self.api_st, x + ) + response = self.session.post(url, headers=self.headers, data=data) + print(response.text) + return response.json() + + def check_captcha(self): + captcha_res = self.verify_captcha() + if captcha_res.get("vt"): + self.vt = captcha_res["vt"] + self.api_st = captcha_res["st"] + return True + self.set_img(captcha_res) + return False + + def build_send_code_data(self): + return self.user_ctw.call( + "sendMsg", + self.phone, + self.vt, + self.jd_risk_token_id, + self.s_token, + self.rsa_modulus, + ) + + def send_code(self): + url = "https://plogin.m.jd.com/cgi-bin/mm/dosendlogincode" + data = self.build_send_code_data() + response = self.session.post(url, headers=self.headers, data=data) + print(response.text) + self.ck = self.get_cookie() + return response.json() + + def get_h5st(self, code): + return self.h5st_ctw.call("getH5st", code, self.s_token) + + def build_verify_data(self, phone_, code, s_token, jd_risk_token_id, rsa_modulus): + h5st = self.get_h5st(code) + return self.user_ctw.call( + "login", phone_, code, s_token, h5st, jd_risk_token_id, rsa_modulus + ) + + def get_cookie(self): + cookie_dict = self.session.cookies.get_dict() + return "; ".join([f"{key}={value}" for key, value in cookie_dict.items()]) + ";" + + def request_sms_login(self, ck, code, s_token, jd_risk_token_id, rsa_modulus): + url = "https://plogin.m.jd.com/cgi-bin/mm/dosmslogin" + h5st = self.get_h5st(code) + data = self.user_ctw.call( + "login", phone, code, s_token, h5st, jd_risk_token_id, rsa_modulus + ) + self.headers["cookie"] = ck + response = requests.post(url, headers=self.headers, data=data) + print(response.text) + self.ck = self.get_cookie() + return response.json() + + def get_jd_risk_token_id(self): + self.jd_risk_token_id = ( + self.request_jd_risk_token_id() + .split("var jd_risk_token_id = ")[1] + .strip(";") + .strip("'") + ) + + def get_code_res(self): + send_res = self.send_code() + if send_res.get("err_code") == 0: + return 100, { + "ck": self.ck, + "s_token": self.s_token, + "jd_risk_token_id": self.jd_risk_token_id, + "rsa_modulus": self.rsa_modulus, + } + else: + return 101, { + "ck": "", + "s_token": "", + "jd_risk_token_id": "", + "rsa_modulus": "", + } + + @staticmethod + def my_response(status_code, data): + return {"code": status_code, "data": data, "msg": "请求成功"} + + def run_get_ck(self, ck, code, s_token, jd_risk_token_id, rsa_modulus): + login_res = self.request_sms_login( + ck, code, s_token, jd_risk_token_id, rsa_modulus + ) + if login_res.get("err_code") == 0: + data = {"ck": self.ck} + return self.my_response(status_code=100, data=data) + else: + data = {"ck": ""} + return self.my_response(status_code=101, data=data) + + def run_send_code(self): + try: + # 获取risk_token_id + self.get_jd_risk_token_id() + # 获取登录所需初始sid + self.get_login_entrance() + self.get_jcap_sid() + # 获取验证码 + self.get_api_params() + captcha_status = self.get_captcha() + + if not captcha_status: + print("出现滑块验证") + # 验证验证码 + check_status = self.check_captcha() + if check_status: + print("滑块验证通过,发送验证码") + status_code, data = self.get_code_res() + return self.my_response(status_code, data) + return None + else: + print("没有出现滑块验证,直接发送验证码") + status_code, data = self.get_code_res() + return self.my_response(status_code, data) + except: + return self.my_response(status_code=111, data={}) + + +if __name__ == "__main__": + phone_ = "13071534209" + res = LoginSpider( + phone_=phone_, + ).run_send_code() + print(res) + + res = LoginSpider( + phone_=phone_, + ).run_get_ck( + ck="jcap_dvzw_fp=7Hq5_QuYK92sDlclFxhJ8m28JaYU1hDYJ-U-gC59LmJRPuo3ERCbiMGzn5vdc3WcTX0ndWGFZNOs3BDenMr7lw==; guid=a5bb2dc650ab23a2cf5cb58afef19b3f35adba9613421b4cba79aa59ea891803; lang=chs; lsid=503346699194megeguncgrf6gvwb9tdujp2md5p27pkt1737276375048; lstoken=vvwcnbi4;", + code="315572", + s_token="vvwcnbi4", + jd_risk_token_id="SH6IDCXBUWZNAV3SJPRGIXLVNHGLZR6AYHJ3244TGU5Z35SFJU76TAQ3OMNTXE7XA3SBRMPVTZJVW", + rsa_modulus="B03744DE9EAB28F6FA6B9C8FB1873CF57D42A2B6D382B79B276C2079A42B24C11D641EA642CF62485A632AE244DE6DE05A92A20EEFE8B6C7743F09FCE0BF78E6D614C115CDAEC2F4825F82E06770A2599D69BBADBE678DD25F2E5B9E2D0E3E15BEB749B436860872D30676794D3C3E8C37B71372DE52F223917FA730EC21F047", + ) + print(res) diff --git a/apps/jd/services/utils.py b/apps/jd/services/utils.py new file mode 100644 index 0000000..f3474ff --- /dev/null +++ b/apps/jd/services/utils.py @@ -0,0 +1,193 @@ +import ctypes +import hashlib +import json +from urllib.parse import parse_qs + +from curl_cffi import requests +from tenacity import retry, stop_after_attempt, wait_exponential + +from observability.logging import get_logger_with_trace + + +logger = get_logger_with_trace(__name__) + + +def r(data_string): + def int_overflow(val): + maxint = 2147483647 + if not -maxint - 1 <= val <= maxint: + val = (val + (maxint + 1)) % (2 * (maxint + 1)) - maxint - 1 + return val + + def unsigned_right_shitf(n, i): + # 数字小于0,则转为32位无符号uint + if n < 0: + n = ctypes.c_uint32(n).value + # 正常位移位数是为正数,但是为了兼容js之类的,负数就右移变成左移好了 + if i < 0: + return -int_overflow(n << abs(i)) + # print(n) + return int_overflow(n >> i) + + char_list = [] + aae = [ + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "U", + "V", + "W", + "X", + "Y", + "Z", + "a", + "b", + "c", + "d", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "+", + "/", + ] + b_arr = data_string.encode("utf-8") + for i in range(0, len(b_arr), 3): + b_arr2 = [None for i in range(4)] + b2 = 0 + for i2 in range(0, 3): + i3 = i + i2 + if i3 <= len(b_arr) - 1: + b_arr2[i2] = b2 | unsigned_right_shitf( + (b_arr[i3] & 255), ((i2 * 2) + 2) + ) + b2 = unsigned_right_shitf( + ((b_arr[i3] & 255) << (((2 - i2) * 2) + 2)) & 255, 2 + ) + else: + b_arr2[i2] = b2 + b2 = 64 + b_arr2[3] = b2 + for i4 in range(4): + if b_arr2[i4] <= 63: + char_list.append(aae[b_arr2[i4]]) + else: + char_list.append("=") + + return "".join(char_list) + + +def encode_cipher(cipher_dict): + for k, v in cipher_dict.items(): + cipher_dict[k] = r(v) + + +def gen_cipher_ep(uuid, ts): + cipher_dict = { + "d_model": "SM-N9760", + "wifiBssid": "unknown", + "osVersion": "9", + "d_brand": "samsung", + "screen": "960*540", + "uuid": uuid, + "aid": uuid, + } + encode_cipher(cipher_dict) + data_dict = { + "hdid": "JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw=", + "ts": ts, + "ridx": -1, + "cipher": cipher_dict, + "ciphertype": 5, + "version": "1.2.0", + "appname": "com.jingdong.app.mall", + } + ep = json.dumps(data_dict, separators=(",", ":")) + + return ep + + +def get_pay_sign(order_id, face_price): + app_id = "jd_android_app4" + pay_app_key = "e53jfgRgd7Hk" + input_str = f"{app_id};{order_id};37;{face_price};{pay_app_key}" + # 获取 MD5 实例 + input_bytes = input_str.encode("GBK") + md5_hash = hashlib.md5() + md5_hash.update(input_bytes) + return md5_hash.hexdigest() + + +@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=15)) +def get_sign(func, body, uuid_, version): + data = { + "func": func, + "body": body, + "uid": uuid_, + "platform": "android", + "version": version, + } + logger.info(f"请求参数:{data}") + response_jd = requests.post( + url="http://unidbg-boot-server:9999/api/jd/encrypt", + json=data, + headers={"Content-Type": "application/json"}, + ) + logger.info(f"请求结果:{response_jd.text}") + res = response_jd.json() + params = parse_qs(res["data"]) + formatted_params = {key: value[0] for key, value in params.items()} + return formatted_params + + +def my_json(code, data, msg): + """返回标准格式的JSON响应""" + # 处理枚举类型 + if hasattr(code, "value"): + code = code.value + return {"code": code, "data": data, "msg": msg} diff --git a/apps/shared/proxy_pool/__init__.py b/apps/shared/proxy_pool/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/shared/proxy_pool/proxy_pool.py b/apps/shared/proxy_pool/proxy_pool.py new file mode 100644 index 0000000..38d273e --- /dev/null +++ b/apps/shared/proxy_pool/proxy_pool.py @@ -0,0 +1,442 @@ +from collections import Counter +import threading +import time +from abc import ABC, abstractmethod +from typing import Optional, Dict, Tuple + +from curl_cffi import ProxySpec, requests + +from core.config import ProxyPoolType, settings +from observability.logging import get_logger_with_trace + +# from app.config.config import Config +# from app.logging.logger import get_logger +# from app.proxy_pool.enums import ProxyPoolType + +# 禁用 HTTPS 请求的警告 +logger = get_logger_with_trace(__name__) + + +class BaseProxyPool(ABC): + """代理池抽象基类""" + + def __init__(self): + self.proxy_timeout = 3 # 代理超时时间,单位秒 + self.test_url = "https://www.baidu.com" + self.max_retries = 1 # 最大重试次数 + self.lock = threading.Lock() # 添加线程锁以确保并发安全 + + @abstractmethod + def get_proxy(self, *args, **kwargs) -> Optional[str]: + """获取代理的抽象方法""" + pass + + @abstractmethod + def release_proxy(self, *args, **kwargs): + """释放代理的抽象方法""" + pass + + @abstractmethod + def _get_new_proxy(self) -> Optional[str]: + """获取新的代理的抽象方法""" + pass + + @abstractmethod + def mark_proxy_invalid(self, proxy: str): + """标记代理为无效的抽象方法""" + pass + + @abstractmethod + def remove_invalid_proxy(self, proxy: str): + """删除指定的无效代理的抽象方法""" + pass + + def _validate_proxy_with_auth(self, proxy: str) -> bool: + """验证带认证信息的代理是否可用 + Args: + proxy: 代理地址,可能是纯IP地址或带认证信息的完整代理地址 + Returns: + bool: 代理是否可用 + """ + # 检查是否已经是带认证信息的代理地址 + if proxy.startswith("http://") and "@" in proxy: + proxyMeta = ProxySpec(all=proxy) + else: + # 如果是纯IP地址,添加认证信息 + proxyMeta: ProxySpec = ProxySpec(all="http://%(user)s:%(pass)s@%(host)s" % { + "host": proxy, + "user": settings.proxy_username, + "pass": settings.proxy_password, + }) + + for attempt in range(self.max_retries): + try: + response = requests.get( + self.test_url, + proxies=proxyMeta, + timeout=self.proxy_timeout, + verify=False, + ) + if response.status_code == 200: + return True + logger.warning( + f"带认证的代理验证失败,状态码: {response.status_code},重试次数: {attempt + 1},代理: {proxyMeta}" + ) + except Exception as e: + logger.warning( + f"带认证的代理验证出错: {str(e)},代理: {proxyMeta},重试次数: {attempt + 1}" + ) + return False + + +class DefaultProxyPool(BaseProxyPool): + """默认代理池实现""" + + def __init__(self): + super().__init__() + self.order_proxy_map: Dict[str, str] = {} # 订单ID -> 代理地址 + self.proxy_order_count_map: Dict[str, int] = {} # 代理地址 -> 使用计数 + self.max_orders_per_proxy = 5 # 一个代理最多可在5个订单中使用 + + def get_proxy(self, order_id: str = "") -> Optional[str]: + """获取指定订单的代理 + Args: + order_id: 订单ID + Returns: + 代理地址,格式为 http://user:pass@ip:port,如果获取失败则返回None + """ + with self.lock: # 使用线程锁确保并发安全 + # 检查是否已有分配的代理 + if order_id in self.order_proxy_map: + proxy = self.order_proxy_map[order_id] + if self._validate_proxy_with_auth(proxy): + return proxy + else: + # 代理无效,清理相关映射 + self._cleanup_proxy_mapping(proxy) + del self.order_proxy_map[order_id] + + # 尝试复用现有代理(使用次数未达上限) + for proxy, count in self.proxy_order_count_map.items(): + if count < self.max_orders_per_proxy and self._validate_proxy_with_auth(proxy): + # 复用此代理 + self.order_proxy_map[order_id] = proxy + self.proxy_order_count_map[proxy] = count + 1 + logger.info(f"订单 {order_id} 复用代理 {proxy},当前使用次数: {count + 1}") + return proxy + + # 获取新代理 + try: + proxy = self._get_new_proxy() + if proxy is not None: + self.order_proxy_map[order_id] = proxy + self.proxy_order_count_map[proxy] = 1 + logger.info(f"订单 {order_id} 获取新代理 {proxy}") + return proxy + except Exception as e: + logger.error(f"获取代理失败: {str(e)}") + return None + + def release_proxy(self, order_id: str): + """释放指定订单的代理 + Args: + order_id: 订单ID + """ + with self.lock: # 使用线程锁确保并发安全 + if order_id in self.order_proxy_map: + proxy = self.order_proxy_map[order_id] + del self.order_proxy_map[order_id] + + # 减少代理使用计数 + if proxy in self.proxy_order_count_map: + self.proxy_order_count_map[proxy] -= 1 + if self.proxy_order_count_map[proxy] <= 0: + del self.proxy_order_count_map[proxy] + logger.info(f"代理 {proxy} 使用计数归零,已从计数映射中删除") + else: + logger.info(f"订单 {order_id} 释放代理 {proxy},剩余使用次数: {self.proxy_order_count_map[proxy]}") + + logger.info(f"订单 {order_id} 已释放代理 {proxy}") + + def _cleanup_proxy_mapping(self, proxy: str): + """清理代理相关的所有映射 + Args: + proxy: 要清理的代理地址 + """ + if proxy in self.proxy_order_count_map: + del self.proxy_order_count_map[proxy] + logger.info(f"已清理代理 {proxy} 的计数映射") + + def mark_proxy_invalid(self, proxy: str): + """标记代理为无效 + Args: + proxy: 要标记为无效的代理地址 + """ + self.remove_invalid_proxy(proxy) + + def remove_invalid_proxy(self, proxy: str): + """删除指定的无效代理 + Args: + proxy: 要删除的代理地址 + """ + with self.lock: + # 查找并删除使用该代理的所有订单映射 + orders_to_remove = [] + for order_id, proxy_addr in self.order_proxy_map.items(): + if proxy_addr == proxy: + orders_to_remove.append(order_id) + + # 删除找到的订单映射 + for order_id in orders_to_remove: + del self.order_proxy_map[order_id] + logger.info(f"已删除订单 {order_id} 的无效代理 {proxy}") + + # 清理代理的计数映射 + self._cleanup_proxy_mapping(proxy) + + if not orders_to_remove: + logger.warning(f"未找到使用代理 {proxy} 的订单,可能已被删除") + + def _get_new_proxy(self) -> Optional[str]: + """获取新的代理,最多尝试3次 + Returns: + 代理地址,格式为 http://user:pass@ip:port + Raises: + Exception: 连续3次获取代理失败时抛出异常 + """ + max_attempts = 2 # 最大尝试次数 + + for attempt in range(max_attempts): + try: + res = requests.get( + settings.proxy_url, + timeout=self.proxy_timeout, + ) + ip = res.text.strip() + if ip and self._validate_proxy_with_auth(ip): + # 构建带认证信息的代理地址 + proxyMeta = "http://%(user)s:%(pass)s@%(host)s" % { + "host": ip, + "user": settings.proxy_username, + "pass": settings.proxy_password, + } + return proxyMeta + logger.warning( + f"获取代理失败或代理验证失败,尝试次数: {attempt + 1}/{max_attempts}" + ) + except Exception as e: + logger.error( + f"获取代理出错: {str(e)},尝试次数: {attempt + 1}/{max_attempts}" + ) + + # 所有尝试都失败,抛出异常 + raise Exception(f"连续{max_attempts}次获取代理失败") + + def get_all_proxies(self) -> Dict[str, str]: + """获取所有订单的代理映射 + Returns: + Dict[str, str]: 订单ID到代理地址的映射 + """ + with self.lock: # 使用线程锁确保并发安全 + return self.order_proxy_map.copy() + + def get_proxy_usage_stats(self) -> Dict[str, int]: + """获取代理使用统计信息 + Returns: + Dict[str, int]: 代理地址到使用次数的映射 + """ + with self.lock: # 使用线程锁确保并发安全 + return self.proxy_order_count_map.copy() + + def set_max_orders_per_proxy(self, max_orders: int): + """设置一个代理最多可使用的订单数 + Args: + max_orders: 最大订单数量 + """ + with self.lock: + self.max_orders_per_proxy = max_orders + logger.info(f"代理最大使用次数已设置为: {max_orders}") + + +class ExpiringProxyPool(BaseProxyPool): + """带有效期的代理池实现""" + + def __init__(self, expire_time: int = 60): + super().__init__() + self.current_proxy: Optional[Tuple[str, float]] = None # 当前代理及其过期时间 + self.proxy_order_count_map = Counter() # 代理使用计数 + self.expire_time = expire_time # 代理有效期 + self.invalid_proxies: set = set() # 存储无效的代理 + self.max_orders_per_proxy = 5 # 一个代理最多可在5个订单中使用 + + def get_proxy(self, order_id: str = "") -> Optional[str]: + """获取指定订单的代理 + Args: + order_id: 订单ID + Returns: + 代理地址,格式为 http://user:pass@ip:port,如果获取失败则返回None + """ + with self.lock: # 使用线程锁确保并发安全 + # 检查当前代理是否有效且未过期 + if self.current_proxy: + proxy, expire_time = self.current_proxy + # 如果代理未过期且不在无效列表中,则返回 + if ( + time.time() < expire_time + and proxy not in self.invalid_proxies + and self._validate_proxy_with_auth(proxy) + and self.proxy_order_count_map[proxy] < self.max_orders_per_proxy + ): + self.proxy_order_count_map[proxy] += 1 + return proxy + + # 获取新代理 + try: + proxy = self._get_new_proxy() + if proxy: + # 设置代理过期时间 + expire_time = time.time() + self.expire_time + self.current_proxy = (proxy, expire_time) + return proxy + except Exception as e: + logger.error(f"获取代理失败: {str(e)}") + return None + return None + + def release_proxy(self, *args, **kwargs): + """释放代理(此实现不需要)""" + pass + + def _get_new_proxy(self) -> Optional[str]: + """获取新的代理,最多尝试3次 + Returns: + 代理地址,格式为 http://user:pass@ip:port + Raises: + Exception: 连续3次获取代理失败时抛出异常 + """ + max_attempts = 2 # 最大尝试次数 + logger.info( + f"获取代理{settings.proxy_url} {settings.proxy_username} {settings.proxy_password}" + ) + + for attempt in range(max_attempts): + try: + res = requests.get( + settings.proxy_url, + timeout=self.proxy_timeout, + ) + ip = res.text.strip() + logger.info(f"获取代理:{ip}") + if ip and self._validate_proxy_with_auth(ip): + # 构建带认证信息的代理地址 + proxyMeta = "http://%(user)s:%(pass)s@%(host)s" % { + "host": ip, + "user": settings.proxy_username, + "pass": settings.proxy_password, + } + # 检查新获取的代理是否在无效列表中 + if proxyMeta not in self.invalid_proxies: + return proxyMeta + logger.warning( + f"获取的代理 {proxyMeta} 在无效列表中,继续尝试获取新代理" + ) + logger.warning( + f"获取代理失败或代理验证失败,尝试次数: {attempt + 1}/{max_attempts}" + ) + except Exception as e: + logger.error( + f"获取代理出错: {str(e)},尝试次数: {attempt + 1}/{max_attempts}" + ) + + # 所有尝试都失败,抛出异常 + raise Exception(f"连续{max_attempts}次获取代理失败") + + def mark_proxy_invalid(self, proxy: str): + """标记代理为无效 + Args: + proxy: 要标记为无效的代理地址 + """ + with self.lock: + # 添加到无效代理列表 + self.invalid_proxies.add(proxy) + # 如果当前代理被标记为无效,立即清除 + if self.current_proxy and self.current_proxy[0] == proxy: + self.current_proxy = None + logger.info(f"代理 {proxy} 已被标记为无效") + + def clear_invalid_proxies(self): + """清除无效代理列表 + 用于定期清理无效代理列表,避免内存占用过大 + """ + with self.lock: + self.invalid_proxies.clear() + logger.info("无效代理列表已清除") + + def remove_invalid_proxy(self, proxy: str): + """删除指定的无效代理 + Args: + proxy: 要删除的代理地址 + """ + with self.lock: + # 从无效代理列表中删除该代理(如果存在) + if proxy in self.invalid_proxies: + self.invalid_proxies.remove(proxy) + logger.info(f"已从无效代理列表中删除代理 {proxy}") + + # 如果当前代理就是要删除的代理,也清除当前代理 + if self.current_proxy and self.current_proxy[0] == proxy: + self.current_proxy = None + logger.info(f"已清除当前无效代理 {proxy}") + + def set_expire_time(self, seconds: int): + """设置代理有效期 + Args: + seconds: 代理有效期,单位秒 + """ + with self.lock: + self.expire_time = seconds + + +class ProxyPoolFactory: + """代理池工厂类""" + + _instances: Dict[ProxyPoolType, BaseProxyPool] = {} + _lock = threading.Lock() + + @classmethod + def get_proxy_pool( + cls, pool_type: ProxyPoolType = ProxyPoolType.DEFAULT, **kwargs + ) -> BaseProxyPool: + """获取代理池实例 + Args: + pool_type: 代理池类型 + **kwargs: 代理池初始化参数 + Returns: + BaseProxyPool: 代理池实例 + """ + with cls._lock: + if pool_type not in cls._instances: + if pool_type == ProxyPoolType.DEFAULT: + cls._instances[pool_type] = DefaultProxyPool() + elif pool_type == ProxyPoolType.EXPIRING: + expire_time = kwargs.get("expire_time", 60) + cls._instances[pool_type] = ExpiringProxyPool( + expire_time=expire_time + ) + return cls._instances[pool_type] + + +# 使用示例 +if __name__ == "__main__": + # 获取默认代理池 + # default_pool = ProxyPoolFactory.get_proxy_pool() + # print(default_pool.get_proxy("test_order")) + + # 获取带有效期的代理池 + for i in range(10): + expiring_pool = ProxyPoolFactory.get_proxy_pool( + ProxyPoolType.EXPIRING, expire_time=60 + ) + proxy = expiring_pool.get_proxy(order_id="test_order") + print(proxy) + time.sleep(2) diff --git a/core/config.py b/core/config.py index 6f2c41d..78f5269 100644 --- a/core/config.py +++ b/core/config.py @@ -3,41 +3,47 @@ Core configuration management using Pydantic Settings. All configuration loaded from environment variables with validation. """ +from enum import StrEnum from typing import Literal from pydantic import Field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict +class ProxyPoolType(StrEnum): + """代理池类型枚举""" + + DEFAULT = "default" # 默认代理池 + EXPIRING = "expiring" # 带有效期的代理池 + + class Settings(BaseSettings): """ Application settings with environment variable support. - + Configuration is loaded from environment variables and .env files. All settings are validated at startup using Pydantic validators. """ - + model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) - + # Application Settings app_name: str = Field(default="kami_spider", description="Application name") environment: Literal["development", "staging", "production"] = Field( - default="development", - description="Runtime environment" + default="development", description="Runtime environment" ) debug: bool = Field(default=False, description="Debug mode") host: str = Field(default="0.0.0.0", description="Server host") port: int = Field(default=8000, description="Server port") workers: int = Field(default=1, description="Number of worker processes") log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field( - default="INFO", - description="Logging level" + default="INFO", description="Logging level" ) - + # Database Settings db_host: str = Field(default="localhost", description="MySQL host") db_port: int = Field(default=3306, description="MySQL port") @@ -45,51 +51,77 @@ class Settings(BaseSettings): db_user: str = Field(default="root", description="Database user") db_password: str = Field(default="", description="Database password") db_pool_size: int = Field(default=10, description="Database connection pool size") - db_max_overflow: int = Field(default=20, description="Database max overflow connections") - db_pool_recycle: int = Field(default=3600, description="Database pool recycle time in seconds") - db_pool_pre_ping: bool = Field(default=True, description="Test connections before using") + db_max_overflow: int = Field( + default=20, description="Database max overflow connections" + ) + db_pool_recycle: int = Field( + default=3600, description="Database pool recycle time in seconds" + ) + db_pool_pre_ping: bool = Field( + default=True, description="Test connections before using" + ) db_echo: bool = Field(default=False, description="Echo SQL statements") - + # Redis Settings redis_host: str = Field(default="localhost", description="Redis host") redis_port: int = Field(default=6379, description="Redis port") redis_db: int = Field(default=0, description="Redis database number") redis_password: str = Field(default="", description="Redis password") - redis_max_connections: int = Field(default=50, description="Redis connection pool max connections") - redis_decode_responses: bool = Field(default=True, description="Decode Redis responses to strings") - + redis_max_connections: int = Field( + default=50, description="Redis connection pool max connections" + ) + redis_decode_responses: bool = Field( + default=True, description="Decode Redis responses to strings" + ) + # OpenTelemetry Settings otel_enabled: bool = Field(default=True, description="Enable OpenTelemetry") - otel_service_name: str = Field(default="kami_spider", description="Service name for traces") + otel_service_name: str = Field( + default="kami_spider", description="Service name for traces" + ) otel_exporter_endpoint: str = Field( default="38.38.251.113:31547", - description="OpenTelemetry collector gRPC endpoint" + description="OpenTelemetry collector gRPC endpoint", ) - otel_exporter_insecure: bool = Field(default=True, description="Use insecure gRPC connection") - otel_sample_rate: float = Field(default=1.0, description="Trace sampling rate (0.0 to 1.0)") - + otel_exporter_insecure: bool = Field( + default=True, description="Use insecure gRPC connection" + ) + otel_sample_rate: float = Field( + default=1.0, description="Trace sampling rate (0.0 to 1.0)" + ) + # CORS Settings cors_enabled: bool = Field(default=True, description="Enable CORS") cors_allow_origins: list[str] = Field( - default=["*"], - description="Allowed CORS origins" + default=["*"], description="Allowed CORS origins" + ) + cors_allow_credentials: bool = Field( + default=True, description="Allow credentials in CORS" ) - cors_allow_credentials: bool = Field(default=True, description="Allow credentials in CORS") cors_allow_methods: list[str] = Field( - default=["*"], - description="Allowed HTTP methods" + default=["*"], description="Allowed HTTP methods" ) cors_allow_headers: list[str] = Field( - default=["*"], - description="Allowed HTTP headers" + default=["*"], description="Allowed HTTP headers" ) - + + # 代理设置 + proxy_enable: bool = Field(default=True, description="是否启用代理") + proxy_url: str = Field( + default="https://share.proxy.qg.net/get?key=7ASQH2BI&num=1&area=&isp=0&format=txt&seq=\r\n&distinct=false&area=510100", + description="代理服务器地址", + ) + proxy_type: ProxyPoolType = Field( + default=ProxyPoolType.DEFAULT, description="代理服务器类型" + ) + proxy_username: str = Field(default="7ASQH2BI", description="代理服务器用户名") + proxy_password: str = Field(default="34D6652FE7B6", description="代理服务器密码") + # Security Settings secret_key: str = Field( - default="change-me-in-production", - description="Secret key for signing tokens" + default="change-me-in-production", description="Secret key for signing tokens" ) - + @field_validator("workers") @classmethod def validate_workers(cls, v: int) -> int: @@ -97,7 +129,7 @@ class Settings(BaseSettings): if v < 1: raise ValueError("workers must be at least 1") return v - + @field_validator("otel_sample_rate") @classmethod def validate_sample_rate(cls, v: float) -> float: @@ -105,7 +137,7 @@ class Settings(BaseSettings): if not 0.0 <= v <= 1.0: raise ValueError("otel_sample_rate must be between 0.0 and 1.0") return v - + @property def database_url(self) -> str: """Generate async database URL for SQLModel/SQLAlchemy.""" @@ -114,7 +146,7 @@ class Settings(BaseSettings): f"mysql+aiomysql://{self.db_user}{password_part}" f"@{self.db_host}:{self.db_port}/{self.db_name}" ) - + @property def sync_database_url(self) -> str: """Generate sync database URL for Alembic migrations.""" @@ -123,18 +155,18 @@ class Settings(BaseSettings): f"mysql+pymysql://{self.db_user}{password_part}" f"@{self.db_host}:{self.db_port}/{self.db_name}" ) - + @property def redis_url(self) -> str: """Generate Redis URL.""" password_part = f":{self.redis_password}@" if self.redis_password else "" return f"redis://{password_part}{self.redis_host}:{self.redis_port}/{self.redis_db}" - + @property def is_production(self) -> bool: """Check if running in production environment.""" return self.environment == "production" - + @property def is_development(self) -> bool: """Check if running in development environment.""" diff --git a/core/exceptions.py b/core/exceptions.py index 35bb707..bf88ee4 100644 --- a/core/exceptions.py +++ b/core/exceptions.py @@ -10,20 +10,20 @@ from core.responses import BusinessCode class BaseAppException(Exception): """ Base exception for all application exceptions. - + Attributes: message: Error message code: Business error code status_code: HTTP status code details: Additional error details """ - + def __init__( self, message: str, - code: int = BusinessCode.UNKNOWN_ERROR, + code: BusinessCode = BusinessCode.UNKNOWN_ERROR, status_code: int = 500, - details: Optional[dict[str, Any]] = None + details: Optional[dict[str, Any]] = None, ): self.message = message self.code = code @@ -35,215 +35,212 @@ class BaseAppException(Exception): class ValidationException(BaseAppException): """ Exception for validation errors. - + HTTP Status: 400 Bad Request Business Code Range: 3000-3999 """ - + def __init__( self, message: str, - code: int = BusinessCode.INVALID_INPUT, - details: Optional[dict[str, Any]] = None + code: BusinessCode = BusinessCode.INVALID_INPUT, + details: Optional[dict[str, Any]] = None, ): - super().__init__( - message=message, - code=code, - status_code=400, - details=details - ) + super().__init__(message=message, code=code, status_code=400, details=details) class NotFoundException(BaseAppException): """ Exception for resource not found errors. - + HTTP Status: 404 Not Found Business Code: 4001 """ - + def __init__( self, message: str, resource: str = "Resource", - details: Optional[dict[str, Any]] = None + details: Optional[dict[str, Any]] = None, ): super().__init__( message=message or f"{resource} not found", code=BusinessCode.RESOURCE_NOT_FOUND, status_code=404, - details=details + details=details, ) class ConflictException(BaseAppException): """ Exception for resource conflict errors. - + HTTP Status: 409 Conflict Business Code: 4003 """ - + def __init__( self, message: str, - code: int = BusinessCode.RESOURCE_CONFLICT, - details: Optional[dict[str, Any]] = None + code: BusinessCode = BusinessCode.RESOURCE_CONFLICT, + details: Optional[dict[str, Any]] = None, ): - super().__init__( - message=message, - code=code, - status_code=409, - details=details - ) + super().__init__(message=message, code=code, status_code=409, details=details) class AlreadyExistsException(BaseAppException): """ Exception for resource already exists errors. - + HTTP Status: 409 Conflict Business Code: 4002 """ - + def __init__( self, message: str, resource: str = "Resource", - details: Optional[dict[str, Any]] = None + details: Optional[dict[str, Any]] = None, ): super().__init__( message=message or f"{resource} already exists", code=BusinessCode.RESOURCE_ALREADY_EXISTS, status_code=409, - details=details + details=details, ) class AuthenticationException(BaseAppException): """ Exception for authentication errors. - + HTTP Status: 401 Unauthorized Business Code Range: 1001-1099 """ - + def __init__( self, message: str, - code: int = BusinessCode.LOGIN_FAILED, - details: Optional[dict[str, Any]] = None + code: BusinessCode = BusinessCode.LOGIN_FAILED, + details: Optional[dict[str, Any]] = None, ): - super().__init__( - message=message, - code=code, - status_code=401, - details=details - ) + super().__init__(message=message, code=code, status_code=401, details=details) class PermissionException(BaseAppException): """ Exception for permission/authorization errors. - + HTTP Status: 403 Forbidden Business Code: 1003 """ - + def __init__( self, message: str, - code: int = BusinessCode.INSUFFICIENT_PERMISSIONS, - details: Optional[dict[str, Any]] = None + code: BusinessCode = BusinessCode.INSUFFICIENT_PERMISSIONS, + details: Optional[dict[str, Any]] = None, ): - super().__init__( - message=message, - code=code, - status_code=403, - details=details - ) + super().__init__(message=message, code=code, status_code=403, details=details) class BusinessLogicException(BaseAppException): """ Exception for business logic errors. - + HTTP Status: 400 Bad Request Business Code Range: 2000-2999 """ - + def __init__( self, message: str, - code: int = BusinessCode.OPERATION_NOT_ALLOWED, - details: Optional[dict[str, Any]] = None + code: BusinessCode = BusinessCode.OPERATION_NOT_ALLOWED, + details: Optional[dict[str, Any]] = None, ): - super().__init__( - message=message, - code=code, - status_code=400, - details=details - ) + super().__init__(message=message, code=code, status_code=400, details=details) class DatabaseException(BaseAppException): """ Exception for database errors. - + HTTP Status: 503 Service Unavailable Business Code: 5001 """ - + def __init__( self, message: str = "Database error occurred", - details: Optional[dict[str, Any]] = None + details: Optional[dict[str, Any]] = None, ): super().__init__( message=message, code=BusinessCode.DATABASE_ERROR, status_code=503, - details=details + details=details, ) class CacheException(BaseAppException): """ Exception for cache/Redis errors. - + HTTP Status: 503 Service Unavailable Business Code: 5003 """ - + def __init__( self, message: str = "Cache service error", - details: Optional[dict[str, Any]] = None + details: Optional[dict[str, Any]] = None, ): super().__init__( message=message, code=BusinessCode.CACHE_ERROR, status_code=503, - details=details + details=details, ) class ExternalServiceException(BaseAppException): """ Exception for external service errors. - + HTTP Status: 502 Bad Gateway Business Code: 5002 """ - + def __init__( self, message: str = "External service unavailable", - details: Optional[dict[str, Any]] = None + details: Optional[dict[str, Any]] = None, ): super().__init__( message=message, code=BusinessCode.EXTERNAL_SERVICE_ERROR, status_code=502, - details=details + details=details, + ) + + +class JDServiceException(BaseAppException): + """ + Exception for external service errors. + + HTTP Status: 502 Bad Gateway + Business Code: 5002 + """ + + def __init__( + self, + code: BusinessCode = BusinessCode.NOT_IMPLEMENTED, + message: str = "External service unavailable", + details: Optional[dict[str, Any]] = None, + ): + super().__init__( + message=message, + code=code, + status_code=200, + details=details, ) diff --git a/core/responses.py b/core/responses.py index a96bcb0..ecef1a4 100644 --- a/core/responses.py +++ b/core/responses.py @@ -6,7 +6,7 @@ Provides unified response structure for all API endpoints. from typing import TypeVar, Generic, Optional, Any from datetime import datetime from pydantic import BaseModel, Field - +from enum import IntEnum T = TypeVar("T") @@ -14,9 +14,9 @@ T = TypeVar("T") class ApiResponse(BaseModel, Generic[T]): """ Unified API response structure. - + Both success and error responses use this structure. - + Attributes: code: Business status code (0 = success, >0 = error) message: Human-readable message @@ -24,28 +24,19 @@ class ApiResponse(BaseModel, Generic[T]): trace_id: Request trace ID for debugging timestamp: Response timestamp in ISO 8601 format """ - + code: int = Field(description="Business status code (0=success, >0=error)") message: str = Field(description="Human-readable message") data: Optional[T] = Field(default=None, description="Response payload") trace_id: str = Field(description="Request trace ID") - timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response timestamp") - - class Config: - json_schema_extra = { - "example": { - "code": 0, - "message": "Success", - "data": {"user_id": 12345, "username": "john_doe"}, - "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2024-01-15T10:30:00Z" - } - } + timestamp: datetime = Field( + default_factory=datetime.utcnow, description="Response timestamp" + ) class PaginationMeta(BaseModel): """Pagination metadata.""" - + total: int = Field(description="Total number of records") page: int = Field(description="Current page number (1-based)") page_size: int = Field(description="Number of records per page") @@ -55,72 +46,69 @@ class PaginationMeta(BaseModel): class PaginatedData(BaseModel, Generic[T]): """ Paginated data structure. - + Attributes: items: List of items for current page pagination: Pagination metadata """ - + items: list[T] = Field(description="List of items") pagination: PaginationMeta = Field(description="Pagination metadata") def success( - data: Optional[T] = None, - message: str = "Success", - trace_id: str = "" + data: Optional[T] = None, message: str = "Success", trace_id: str = "" ) -> ApiResponse[T]: """ Create success response. - + Args: data: Response payload message: Success message trace_id: Request trace ID - + Returns: ApiResponse: Success response with code=0 - + Example: return success(data={"user_id": 123}, message="User created") """ return ApiResponse( - code=0, + code=BusinessCode.SUCCESS, message=message, data=data, trace_id=trace_id, - timestamp=datetime.utcnow() + timestamp=datetime.now(), ) -def error( - code: int, - message: str, - trace_id: str = "" -) -> ApiResponse[None]: +def error(code: "BusinessCode", data: Any=None, message: str="", trace_id: str = "") -> ApiResponse[None]: """ Create error response. - + Args: code: Business error code (must be > 0) message: Error message trace_id: Request trace ID - + Returns: - ApiResponse: Error response with data=None - + ApiResponse: Error response + Example: return error(code=1001, message="Login failed: Invalid credentials") """ if code <= 0: raise ValueError("Error code must be greater than 0") - + + if message == "": + message = BusinessCode(code).name + return ApiResponse( code=code, message=message, - data=None, + data=data, trace_id=trace_id, - timestamp=datetime.utcnow() + timestamp=datetime.now(), ) @@ -130,11 +118,11 @@ def paginated( page: int, page_size: int, message: str = "Success", - trace_id: str = "" + trace_id: str = "", ) -> ApiResponse[PaginatedData[T]]: """ Create paginated response. - + Args: items: List of items for current page total: Total number of records @@ -142,10 +130,10 @@ def paginated( page_size: Number of records per page message: Success message trace_id: Request trace ID - + Returns: ApiResponse: Paginated response - + Example: return paginated( items=[user1, user2], @@ -155,33 +143,27 @@ def paginated( ) """ total_pages = (total + page_size - 1) // page_size if page_size > 0 else 0 - + pagination_meta = PaginationMeta( - total=total, - page=page, - page_size=page_size, - total_pages=total_pages + total=total, page=page, page_size=page_size, total_pages=total_pages ) - - paginated_data = PaginatedData( - items=items, - pagination=pagination_meta - ) - + + paginated_data = PaginatedData(items=items, pagination=pagination_meta) + return ApiResponse( code=0, message=message, data=paginated_data, trace_id=trace_id, - timestamp=datetime.utcnow() + timestamp=datetime.utcnow(), ) # Business code constants -class BusinessCode: +class BusinessCode(IntEnum): """ Business status code definitions. - + Code ranges: 0: Success 1000-1999: Authentication & Authorization @@ -191,40 +173,53 @@ class BusinessCode: 5000-5999: System Errors 9000-9999: Unknown Errors """ - + # Success SUCCESS = 0 - + # Authentication & Authorization (1000-1999) LOGIN_FAILED = 1001 TOKEN_EXPIRED = 1002 INSUFFICIENT_PERMISSIONS = 1003 INVALID_TOKEN = 1004 - + # Business Logic Errors (2000-2999) ORDER_CREATION_FAILED = 2001 PAYMENT_FAILED = 2002 INSUFFICIENT_BALANCE = 2003 OPERATION_NOT_ALLOWED = 2004 - + # Validation Errors (3000-3999) INVALID_INPUT = 3001 MISSING_REQUIRED_FIELD = 3002 INVALID_FORMAT = 3003 - + # Resource Errors (4000-4999) RESOURCE_NOT_FOUND = 4001 RESOURCE_ALREADY_EXISTS = 4002 RESOURCE_CONFLICT = 4003 - + # System Errors (5000-5999) DATABASE_ERROR = 5001 EXTERNAL_SERVICE_ERROR = 5002 CACHE_ERROR = 5003 - + INTERNAL_ERROR = 5004 + + # 没有实现 + NOT_IMPLEMENTED = 5004 + # Unknown Errors (9000-9999) UNKNOWN_ERROR = 9000 + # 京东充值的错误 + JD_ORDER_FACE_PRICE_ERR = 10001 + JD_ORDER_NORMAL_ERR = 10002 + JD_ORDER_CK_ERR = 10003 + JD_ORDER_TYPE_NOT_SUPPORTED_ERR = 10004 + JD_ORDER_EXPIRED_ERR = 10005 + JD_ORDER_STOCK_ERR = 10006 + JD_ORDER_RISK_ERR = 10007 + # Common error response examples for OpenAPI documentation ERROR_RESPONSES = { @@ -237,10 +232,10 @@ ERROR_RESPONSES = { "message": "Validation error: email - field required", "data": None, "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2024-01-15T10:30:00Z" + "timestamp": "2024-01-15T10:30:00Z", } } - } + }, }, 401: { "description": "Unauthorized - Authentication failed", @@ -251,10 +246,10 @@ ERROR_RESPONSES = { "message": "Login failed: Invalid credentials", "data": None, "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2024-01-15T10:30:00Z" + "timestamp": "2024-01-15T10:30:00Z", } } - } + }, }, 403: { "description": "Forbidden - Insufficient permissions", @@ -265,10 +260,10 @@ ERROR_RESPONSES = { "message": "Insufficient permissions to perform this action", "data": None, "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2024-01-15T10:30:00Z" + "timestamp": "2024-01-15T10:30:00Z", } } - } + }, }, 404: { "description": "Not Found - Resource does not exist", @@ -279,10 +274,10 @@ ERROR_RESPONSES = { "message": "User not found", "data": None, "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2024-01-15T10:30:00Z" + "timestamp": "2024-01-15T10:30:00Z", } } - } + }, }, 409: { "description": "Conflict - Resource already exists or conflicts", @@ -293,10 +288,10 @@ ERROR_RESPONSES = { "message": "User already exists", "data": None, "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2024-01-15T10:30:00Z" + "timestamp": "2024-01-15T10:30:00Z", } } - } + }, }, 422: { "description": "Unprocessable Entity - Validation error", @@ -307,10 +302,10 @@ ERROR_RESPONSES = { "message": "Validation error: body -> email - value is not a valid email address", "data": None, "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2024-01-15T10:30:00Z" + "timestamp": "2024-01-15T10:30:00Z", } } - } + }, }, 500: { "description": "Internal Server Error - Unexpected error", @@ -321,10 +316,10 @@ ERROR_RESPONSES = { "message": "An unexpected error occurred", "data": None, "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2024-01-15T10:30:00Z" + "timestamp": "2024-01-15T10:30:00Z", } } - } + }, }, 502: { "description": "Bad Gateway - External service error", @@ -335,10 +330,10 @@ ERROR_RESPONSES = { "message": "External service unavailable", "data": None, "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2024-01-15T10:30:00Z" + "timestamp": "2024-01-15T10:30:00Z", } } - } + }, }, 503: { "description": "Service Unavailable - Database or cache error", @@ -349,10 +344,10 @@ ERROR_RESPONSES = { "message": "Database error occurred", "data": None, "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - "timestamp": "2024-01-15T10:30:00Z" + "timestamp": "2024-01-15T10:30:00Z", } } - } + }, }, } @@ -360,33 +355,33 @@ ERROR_RESPONSES = { # Error message templates class ErrorMessage: """Predefined error messages for common errors.""" - + # Authentication LOGIN_FAILED = "Login failed: Invalid credentials" TOKEN_EXPIRED = "Authentication token has expired" INSUFFICIENT_PERMISSIONS = "Insufficient permissions to perform this action" INVALID_TOKEN = "Invalid authentication token" - + # Business Logic ORDER_CREATION_FAILED = "Order creation failed: {reason}" PAYMENT_FAILED = "Payment processing failed: {reason}" INSUFFICIENT_BALANCE = "Insufficient account balance" OPERATION_NOT_ALLOWED = "Operation not allowed: {reason}" - + # Validation INVALID_INPUT = "Invalid input: {field}" MISSING_REQUIRED_FIELD = "Missing required field: {field}" INVALID_FORMAT = "Invalid format: {field}" - + # Resources RESOURCE_NOT_FOUND = "{resource} not found" RESOURCE_ALREADY_EXISTS = "{resource} already exists" RESOURCE_CONFLICT = "{resource} conflict: {reason}" - + # System DATABASE_ERROR = "Database error occurred" EXTERNAL_SERVICE_ERROR = "External service unavailable" CACHE_ERROR = "Cache service error" - + # Unknown UNKNOWN_ERROR = "An unexpected error occurred" diff --git a/main.py b/main.py index 80c403f..1ef912d 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ Bootstraps the application with middleware, routers, and lifecycle handlers. """ from contextlib import asynccontextmanager -from fastapi import FastAPI +from fastapi import APIRouter, FastAPI from fastapi.responses import ORJSONResponse from fastapi.middleware.cors import CORSMiddleware from core.config import settings @@ -16,8 +16,10 @@ from observability.logging import setup_logging, get_logger from middleware.trace_context import TraceContextMiddleware from middleware.logging import RequestLoggingMiddleware from middleware.error_handler import register_exception_handlers -from apps.app_a.router import router as app_a_router -from apps.apple.router import router as app_b_router + +# from apps.app_a.router import router as app_a_router +# from apps.apple.router import router as app_b_router +from apps.jd.router import router as jd_router logger = get_logger(__name__) @@ -26,51 +28,53 @@ logger = get_logger(__name__) async def lifespan(app: FastAPI): """ Application lifespan manager. - + Handles startup and shutdown events. """ # Startup logger.info("Starting application...") - + # Setup logging setup_logging() logger.info(f"Logging configured: level={settings.log_level}") - + # Initialize OpenTelemetry if settings.otel_enabled: init_tracing() instrument_app(app) - logger.info(f"OpenTelemetry initialized: endpoint={settings.otel_exporter_endpoint}") - + logger.info( + f"OpenTelemetry initialized: endpoint={settings.otel_exporter_endpoint}" + ) + # Initialize Redis await init_redis() logger.info(f"Redis initialized: {settings.redis_host}:{settings.redis_port}") - + # Create database tables (development only) if settings.is_development: await create_db_and_tables() logger.info("Database tables created") - + logger.info(f"Application started: environment={settings.environment}") - + yield - + # Shutdown logger.info("Shutting down application...") - + # Close Redis connection await close_redis_connection() logger.info("Redis connection closed") - + # Close database connection await close_database_connection() logger.info("Database connection closed") - + # Shutdown OpenTelemetry if settings.otel_enabled: await shutdown_tracing() logger.info("OpenTelemetry shutdown") - + logger.info("Application shutdown complete") @@ -104,10 +108,9 @@ app.add_middleware(TraceContextMiddleware) # Register exception handlers register_exception_handlers(app) -# Include application routers -app.include_router(app_a_router) -app.include_router(app_b_router) - +router = APIRouter(prefix="/api") +router.include_router(jd_router()) +app.include_router(router) # Health check endpoint @app.get( @@ -119,26 +122,26 @@ app.include_router(app_b_router) 200: {"description": "Service is healthy"}, 500: ERROR_RESPONSES[500], 503: ERROR_RESPONSES[503], - } + }, ) async def health_check(): """ Health check endpoint. - + Returns health status of the application and its components. """ from core.database import check_database_connection from core.redis import check_redis_connection - + # Check database db_healthy = await check_database_connection() - + # Check Redis redis_healthy = await check_redis_connection() - + # Overall health healthy = db_healthy and redis_healthy - + return { "status": "healthy" if healthy else "unhealthy", "components": { @@ -159,7 +162,7 @@ async def health_check(): description="Get API information", responses={ 200: {"description": "API information retrieved successfully"}, - } + }, ) async def root(): """Root endpoint with API information.""" @@ -174,7 +177,7 @@ async def root(): if __name__ == "__main__": import uvicorn - + uvicorn.run( "main:app", host=settings.host, diff --git a/pyproject.toml b/pyproject.toml index c7ad240..b4e5c93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,12 @@ dependencies = [ "brotli>=1.1.0", "psutil>=5.9.0", "pycryptodome>=3.21.0", + "curl-cffi>=0.13.0", + "fake-useragent>=2.2.0", + "opentelemetry-instrumentation-requests>=0.59b0", + "ddddocr>=1.5.6", + "tenacity>=9.1.2", + "pyexecjs>=1.5.1", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index 3eaeb83..9d65844 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,11 @@ version = 1 revision = 2 requires-python = ">=3.13" +resolution-markers = [ + "sys_platform == 'darwin'", + "platform_machine == 'aarch64' and sys_platform == 'linux'", + "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')", +] [[package]] name = "aiomysql" @@ -68,6 +73,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/9c/fc2331f538fbf7eedba64b2052e99ccf9ba9d6888e2f41441ee28847004b/asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", size = 24050, upload-time = "2025-10-05T09:15:05.11Z" }, ] +[[package]] +name = "brotli" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload-time = "2023-09-07T14:05:41.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681, upload-time = "2024-10-18T12:32:34.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475, upload-time = "2024-10-18T12:32:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173, upload-time = "2024-10-18T12:32:37.978Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803, upload-time = "2024-10-18T12:32:39.606Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946, upload-time = "2024-10-18T12:32:41.679Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707, upload-time = "2024-10-18T12:32:43.478Z" }, + { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231, upload-time = "2024-10-18T12:32:45.224Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157, upload-time = "2024-10-18T12:32:46.894Z" }, + { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122, upload-time = "2024-10-18T12:32:48.844Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206, upload-time = "2024-10-18T12:32:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804, upload-time = "2024-10-18T12:32:52.661Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517, upload-time = "2024-10-18T12:32:54.066Z" }, +] + [[package]] name = "certifi" version = "2025.10.5" @@ -143,6 +168,18 @@ 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 = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, +] + [[package]] name = "coverage" version = "7.11.0" @@ -260,6 +297,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, ] +[[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 = "ddddocr" +version = "1.5.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "onnxruntime" }, + { name = "opencv-python-headless" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/cf/1243d5f0d03763a287375366f68eadb5c14418f5b3df00c09eb971e526a7/ddddocr-1.5.6.tar.gz", hash = "sha256:2839a940bfabe02e3284ef3f9d2a037292aa9f641f355b43a9b70bece9e1b73d", size = 75825027, upload-time = "2024-10-15T09:22:00.94Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/74/418c1c0be49463799f9eeb307a8aa4013ff5fca5e0387f0ef2762fcdb4e2/ddddocr-1.5.6-py3-none-any.whl", hash = "sha256:f13865b00e42de5c2507c1889ba73c2bacd218a49d15b928c2a5c82667062ac5", size = 75868010, upload-time = "2024-10-15T09:21:41.061Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -282,6 +355,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "fake-useragent" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/43/948d10bf42735709edb5ae51e23297d034086f17fc7279fef385a7acb473/fake_useragent-2.2.0.tar.gz", hash = "sha256:4e6ab6571e40cc086d788523cf9e018f618d07f9050f822ff409a4dfe17c16b2", size = 158898, upload-time = "2025-04-14T15:32:19.238Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/37/b3ea9cd5558ff4cb51957caca2193981c6b0ff30bd0d2630ac62505d99d0/fake_useragent-2.2.0-py3-none-any.whl", hash = "sha256:67f35ca4d847b0d298187443aaf020413746e56acd985a611908c73dba2daa24", size = 161695, upload-time = "2025-04-14T15:32:17.732Z" }, +] + [[package]] name = "fastapi" version = "0.120.0" @@ -297,6 +379,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/60/7a639ceaba54aec4e1d5676498c568abc654b95762d456095b6cb529b1ca/fastapi-0.120.0-py3-none-any.whl", hash = "sha256:84009182e530c47648da2f07eb380b44b69889a4acfd9e9035ee4605c5cfc469", size = 108243, upload-time = "2025-10-23T20:56:33.281Z" }, ] +[[package]] +name = "flatbuffers" +version = "25.9.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1f/3ee70b0a55137442038f2a33469cc5fddd7e0ad2abf83d7497c18a2b6923/flatbuffers-25.9.23.tar.gz", hash = "sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12", size = 22067, upload-time = "2025-09-24T05:25:30.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/1b/00a78aa2e8fbd63f9af08c9c19e6deb3d5d66b4dda677a0f61654680ee89/flatbuffers-25.9.23-py2.py3-none-any.whl", hash = "sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2", size = 30869, upload-time = "2025-09-24T05:25:28.912Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.71.0" @@ -435,6 +526,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -472,8 +575,12 @@ source = { editable = "." } dependencies = [ { name = "aiomysql" }, { name = "alembic" }, + { name = "brotli" }, { name = "cryptography" }, + { name = "curl-cffi" }, + { name = "ddddocr" }, { name = "email-validator" }, + { name = "fake-useragent" }, { name = "fastapi" }, { name = "greenlet" }, { name = "gunicorn" }, @@ -483,16 +590,21 @@ dependencies = [ { name = "opentelemetry-instrumentation-fastapi" }, { name = "opentelemetry-instrumentation-httpx" }, { name = "opentelemetry-instrumentation-redis" }, + { name = "opentelemetry-instrumentation-requests" }, { name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "opentelemetry-sdk" }, { name = "orjson" }, + { name = "psutil" }, + { name = "pycryptodome" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pyexecjs" }, { name = "pymysql" }, { name = "python-dotenv" }, { name = "python-multipart" }, { name = "redis" }, { name = "sqlmodel" }, + { name = "tenacity" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -511,8 +623,12 @@ dev = [ requires-dist = [ { name = "aiomysql", specifier = ">=0.2.0" }, { name = "alembic", specifier = ">=1.14.0" }, + { name = "brotli", specifier = ">=1.1.0" }, { name = "cryptography", specifier = ">=46.0.1" }, + { name = "curl-cffi", specifier = ">=0.13.0" }, + { name = "ddddocr", specifier = ">=1.5.6" }, { name = "email-validator", specifier = ">=2.3.0" }, + { name = "fake-useragent", specifier = ">=2.2.0" }, { name = "fastapi", specifier = ">=0.120.0" }, { name = "greenlet", specifier = ">=3.2.4" }, { name = "gunicorn", specifier = ">=23.0.0" }, @@ -524,11 +640,15 @@ requires-dist = [ { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.59b0" }, { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.59b0" }, { name = "opentelemetry-instrumentation-redis", specifier = ">=0.59b0" }, + { name = "opentelemetry-instrumentation-requests", specifier = ">=0.59b0" }, { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.59b0" }, { name = "opentelemetry-sdk", specifier = ">=1.38.0" }, { name = "orjson", specifier = ">=3.11.4" }, + { name = "psutil", specifier = ">=5.9.0" }, + { name = "pycryptodome", specifier = ">=3.21.0" }, { name = "pydantic", specifier = ">=2.12.3" }, { name = "pydantic-settings", specifier = ">=2.11.0" }, + { name = "pyexecjs", specifier = ">=1.5.1" }, { name = "pymysql", specifier = ">=1.1.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.4" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" }, @@ -539,6 +659,7 @@ requires-dist = [ { name = "redis", specifier = ">=5.2.1" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.4" }, { name = "sqlmodel", specifier = ">=0.0.24" }, + { name = "tenacity", specifier = ">=9.1.2" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.38.0" }, ] provides-extras = ["dev"] @@ -607,6 +728,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + [[package]] name = "mypy" version = "1.18.2" @@ -642,6 +772,97 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "numpy" +version = "2.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, + { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, + { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, + { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, + { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, + { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, + { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, + { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, + { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, + { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, + { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, + { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, + { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, + { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, + { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337, upload-time = "2025-10-22T03:46:35.168Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691, upload-time = "2025-10-22T03:46:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898, upload-time = "2025-10-22T03:46:30.039Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518, upload-time = "2025-10-22T03:47:05.407Z" }, + { url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276, upload-time = "2025-10-22T03:47:31.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610, upload-time = "2025-10-22T03:46:32.239Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184, upload-time = "2025-10-22T03:47:08.127Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/53/2c50afa0b1e05ecdb4603818e85f7d174e683d874ef63a6abe3ac92220c8/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca", size = 37326460, upload-time = "2025-01-16T13:52:57.015Z" }, + { url = "https://files.pythonhosted.org/packages/3b/43/68555327df94bb9b59a1fd645f63fafb0762515344d2046698762fc19d58/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81", size = 56723330, upload-time = "2025-01-16T13:55:45.731Z" }, + { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060, upload-time = "2025-01-16T13:51:59.625Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856, upload-time = "2025-01-16T13:53:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425, upload-time = "2025-01-16T13:52:49.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386, upload-time = "2025-01-16T13:52:56.418Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.38.0" @@ -763,6 +984,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/87/fef04827239ce84e2729b11611e8d5be7892288f620961ee9b9bafd035c5/opentelemetry_instrumentation_redis-0.59b0-py3-none-any.whl", hash = "sha256:8f7494dede5a6bfe5d8f20da67b371a502883398081856378380efef27da0bdf", size = 14946, upload-time = "2025-10-16T08:39:07.887Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-requests" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/01/31282a46b09684dfc636bc066deb090bae6973e71e85e253a8c74e727b1f/opentelemetry_instrumentation_requests-0.59b0.tar.gz", hash = "sha256:9af2ffe3317f03074d7f865919139e89170b6763a0251b68c25e8e64e04b3400", size = 15186, upload-time = "2025-10-16T08:40:00.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ea/c282ba418b2669e4f730cb3f68b02a0ca65f4baf801e971169a4cc449ffb/opentelemetry_instrumentation_requests-0.59b0-py3-none-any.whl", hash = "sha256:d43121532877e31a46c48649279cec2504ee1e0ceb3c87b80fe5ccd7eafc14c1", size = 12966, upload-time = "2025-10-16T08:39:09.919Z" }, +] + [[package]] name = "opentelemetry-instrumentation-sqlalchemy" version = "0.59b0" @@ -883,6 +1119,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -907,6 +1201,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/d1/0a28c21707807c6aacd5dc9c3704b2aa1effbf37adebd8caeaf68b17a636/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", size = 170477, upload-time = "2025-10-15T20:39:51.311Z" }, ] +[[package]] +name = "psutil" +version = "7.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751, upload-time = "2025-11-02T12:25:58.161Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, + { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, + { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -916,6 +1236,36 @@ 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" }, +] + [[package]] name = "pydantic" version = "2.12.3" @@ -994,6 +1344,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, ] +[[package]] +name = "pyexecjs" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/8e/aedef81641c8dca6fd0fb7294de5bed9c45f3397d67fddf755c1042c2642/PyExecJS-1.5.1.tar.gz", hash = "sha256:34cc1d070976918183ff7bdc0ad71f8157a891c92708c00c5fbbff7a769f505c", size = 13344, upload-time = "2018-01-18T04:33:55.126Z" } + [[package]] name = "pygments" version = "2.19.2" @@ -1012,6 +1371,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, ] +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -1155,6 +1523,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1210,6 +1587,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"