feat(jd): 添加京东相关路由及苹果权益充值功能

- 新增jd模块基础路由,整合app_store和payment子路由
- 实现苹果权益充值接口,支持苹果、携程及沃尔玛多个渠道
- 实现卡号密码查询接口,支持不同类别订单查询
- 新增短信认证相关接口,实现短信验证码发送及短信登录
- 新增商品管理接口,支持SKU详情查询及账号类下单功能
- 新增订单管理接口,实现订单删除功能
- 实现支付相关接口,增加刷新支付参数功能
- 定义完整请求及响应数据模型,确保接口数据规范
- 编写AppStoreSpider类,封装苹果应用内订单处理逻辑
- 引入多种代理池及请求重试机制,增强接口稳定性
- 添加详细日志记录,便于请求追踪与错误排查
This commit is contained in:
danial
2025-11-03 19:35:39 +08:00
parent ba558b94ad
commit 6c768b6e7b
23 changed files with 4892 additions and 227 deletions

View File

@@ -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

216
apps/jd/router/app_store.py Normal file
View File

@@ -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:
# 10010164405229636
# 20010164405699325
# 30010164405273963
# 50010164404344795
# 20010155862962901
# 30010155867355510
# 50010155863307550
# 100010155867606890
# 100010164403489305
# 100010157256668464
# 50010157256668463
# 30010157256668462
# 20010157256668461
# 10010157256668460
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="查询异常")

41
apps/jd/router/auth.py Normal file
View File

@@ -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

65
apps/jd/router/goods.py Normal file
View File

@@ -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="请求失败")

25
apps/jd/router/order.py Normal file
View File

@@ -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

96
apps/jd/router/payment.py Normal file
View File

@@ -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="请求失败")

110
apps/jd/schemas/models.py Normal file
View File

@@ -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="充值类型 1username账号充值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="备注")

View File

@@ -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']}&timestamp={pay_info['timeStamp']}"
)
logger.info(f"获取微信app端支付参数返回{wx_pay_info}")
return BusinessCode.SUCCESS, wx_pay_info

817
apps/jd/services/ctrip.py Normal file
View File

@@ -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),
}

237
apps/jd/services/delete.py Normal file
View File

@@ -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()

View File

@@ -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()

View File

@@ -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,
}

690
apps/jd/services/jstk.py Normal file
View File

@@ -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<N01c7KvwZO56RSTAfghiFyzWJqVabGH4PQdopUrsCuX*xeBjkltDEmn89.-"
)
# JS代码中索引 64 被用于填充字符。虽然字母表里没有 '=',
# 但标准Base64用 '=' 填充且JS代码逻辑中使用了 64推测 64 对应填充字符 '='
padding_char = "="
# 2. 将输入 e 转换为 JSON 字符串
s = json.dumps(e, separators=(",", ":"))
# 3. 对 JSON 字符串进行 URL 编码
# encodeURIComponent 编码所有字符,除了 ASCII 字母、数字、- _ . ! ~ * ' ( )
# Python 的 urllib.parse.quote 需要指定 safe 字符集来模拟
s_encoded = urllib.parse.quote(
s,
safe="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'()",
)
# 获取 URL 编码后字符串的字节表示,因为 Base64 编码是基于字节的
s_bytes = s_encoded.encode("utf-8")
# 4. 执行自定义的 Base64 编码
encoded_string = ""
d = 0 # 字节索引
s_len = len(s_bytes)
while d < s_len:
# 取 3 个字节(可能不足 3 个在末尾)
byte1 = s_bytes[d]
d += 1
byte2 = s_bytes[d] if d < s_len else None
d += 1
byte3 = s_bytes[d] if d < s_len else None
d += 1
# 将 3 个字节24 位)分成 4 组,每组 6 位
# 索引 1: byte1 的前 6 位
idx1 = (byte1 >> 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)

329
apps/jd/services/login.py Normal file
View File

@@ -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)

193
apps/jd/services/utils.py Normal file
View File

@@ -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}

View File

View File

@@ -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)

View File

@@ -3,11 +3,19 @@ Core configuration management using Pydantic Settings.
All configuration loaded from environment variables with validation. All configuration loaded from environment variables with validation.
""" """
from enum import StrEnum
from typing import Literal from typing import Literal
from pydantic import Field, field_validator from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
class ProxyPoolType(StrEnum):
"""代理池类型枚举"""
DEFAULT = "default" # 默认代理池
EXPIRING = "expiring" # 带有效期的代理池
class Settings(BaseSettings): class Settings(BaseSettings):
""" """
Application settings with environment variable support. Application settings with environment variable support.
@@ -26,16 +34,14 @@ class Settings(BaseSettings):
# Application Settings # Application Settings
app_name: str = Field(default="kami_spider", description="Application name") app_name: str = Field(default="kami_spider", description="Application name")
environment: Literal["development", "staging", "production"] = Field( environment: Literal["development", "staging", "production"] = Field(
default="development", default="development", description="Runtime environment"
description="Runtime environment"
) )
debug: bool = Field(default=False, description="Debug mode") debug: bool = Field(default=False, description="Debug mode")
host: str = Field(default="0.0.0.0", description="Server host") host: str = Field(default="0.0.0.0", description="Server host")
port: int = Field(default=8000, description="Server port") port: int = Field(default=8000, description="Server port")
workers: int = Field(default=1, description="Number of worker processes") workers: int = Field(default=1, description="Number of worker processes")
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field( log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
default="INFO", default="INFO", description="Logging level"
description="Logging level"
) )
# Database Settings # Database Settings
@@ -45,9 +51,15 @@ class Settings(BaseSettings):
db_user: str = Field(default="root", description="Database user") db_user: str = Field(default="root", description="Database user")
db_password: str = Field(default="", description="Database password") db_password: str = Field(default="", description="Database password")
db_pool_size: int = Field(default=10, description="Database connection pool size") 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_max_overflow: int = Field(
db_pool_recycle: int = Field(default=3600, description="Database pool recycle time in seconds") default=20, description="Database max overflow connections"
db_pool_pre_ping: bool = Field(default=True, description="Test connections before using") )
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") db_echo: bool = Field(default=False, description="Echo SQL statements")
# Redis Settings # Redis Settings
@@ -55,39 +67,59 @@ class Settings(BaseSettings):
redis_port: int = Field(default=6379, description="Redis port") redis_port: int = Field(default=6379, description="Redis port")
redis_db: int = Field(default=0, description="Redis database number") redis_db: int = Field(default=0, description="Redis database number")
redis_password: str = Field(default="", description="Redis password") redis_password: str = Field(default="", description="Redis password")
redis_max_connections: int = Field(default=50, description="Redis connection pool max connections") redis_max_connections: int = Field(
redis_decode_responses: bool = Field(default=True, description="Decode Redis responses to strings") default=50, description="Redis connection pool max connections"
)
redis_decode_responses: bool = Field(
default=True, description="Decode Redis responses to strings"
)
# OpenTelemetry Settings # OpenTelemetry Settings
otel_enabled: bool = Field(default=True, description="Enable OpenTelemetry") 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( otel_exporter_endpoint: str = Field(
default="38.38.251.113:31547", 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 Settings
cors_enabled: bool = Field(default=True, description="Enable CORS") cors_enabled: bool = Field(default=True, description="Enable CORS")
cors_allow_origins: list[str] = Field( cors_allow_origins: list[str] = Field(
default=["*"], default=["*"], description="Allowed CORS origins"
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( cors_allow_methods: list[str] = Field(
default=["*"], default=["*"], description="Allowed HTTP methods"
description="Allowed HTTP methods"
) )
cors_allow_headers: list[str] = Field( cors_allow_headers: list[str] = Field(
default=["*"], default=["*"], description="Allowed HTTP headers"
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 # Security Settings
secret_key: str = Field( secret_key: str = Field(
default="change-me-in-production", default="change-me-in-production", description="Secret key for signing tokens"
description="Secret key for signing tokens"
) )
@field_validator("workers") @field_validator("workers")

View File

@@ -21,9 +21,9 @@ class BaseAppException(Exception):
def __init__( def __init__(
self, self,
message: str, message: str,
code: int = BusinessCode.UNKNOWN_ERROR, code: BusinessCode = BusinessCode.UNKNOWN_ERROR,
status_code: int = 500, status_code: int = 500,
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
self.message = message self.message = message
self.code = code self.code = code
@@ -43,15 +43,10 @@ class ValidationException(BaseAppException):
def __init__( def __init__(
self, self,
message: str, message: str,
code: int = BusinessCode.INVALID_INPUT, code: BusinessCode = BusinessCode.INVALID_INPUT,
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
super().__init__( super().__init__(message=message, code=code, status_code=400, details=details)
message=message,
code=code,
status_code=400,
details=details
)
class NotFoundException(BaseAppException): class NotFoundException(BaseAppException):
@@ -66,13 +61,13 @@ class NotFoundException(BaseAppException):
self, self,
message: str, message: str,
resource: str = "Resource", resource: str = "Resource",
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
super().__init__( super().__init__(
message=message or f"{resource} not found", message=message or f"{resource} not found",
code=BusinessCode.RESOURCE_NOT_FOUND, code=BusinessCode.RESOURCE_NOT_FOUND,
status_code=404, status_code=404,
details=details details=details,
) )
@@ -87,15 +82,10 @@ class ConflictException(BaseAppException):
def __init__( def __init__(
self, self,
message: str, message: str,
code: int = BusinessCode.RESOURCE_CONFLICT, code: BusinessCode = BusinessCode.RESOURCE_CONFLICT,
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
super().__init__( super().__init__(message=message, code=code, status_code=409, details=details)
message=message,
code=code,
status_code=409,
details=details
)
class AlreadyExistsException(BaseAppException): class AlreadyExistsException(BaseAppException):
@@ -110,13 +100,13 @@ class AlreadyExistsException(BaseAppException):
self, self,
message: str, message: str,
resource: str = "Resource", resource: str = "Resource",
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
super().__init__( super().__init__(
message=message or f"{resource} already exists", message=message or f"{resource} already exists",
code=BusinessCode.RESOURCE_ALREADY_EXISTS, code=BusinessCode.RESOURCE_ALREADY_EXISTS,
status_code=409, status_code=409,
details=details details=details,
) )
@@ -131,15 +121,10 @@ class AuthenticationException(BaseAppException):
def __init__( def __init__(
self, self,
message: str, message: str,
code: int = BusinessCode.LOGIN_FAILED, code: BusinessCode = BusinessCode.LOGIN_FAILED,
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
super().__init__( super().__init__(message=message, code=code, status_code=401, details=details)
message=message,
code=code,
status_code=401,
details=details
)
class PermissionException(BaseAppException): class PermissionException(BaseAppException):
@@ -153,15 +138,10 @@ class PermissionException(BaseAppException):
def __init__( def __init__(
self, self,
message: str, message: str,
code: int = BusinessCode.INSUFFICIENT_PERMISSIONS, code: BusinessCode = BusinessCode.INSUFFICIENT_PERMISSIONS,
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
super().__init__( super().__init__(message=message, code=code, status_code=403, details=details)
message=message,
code=code,
status_code=403,
details=details
)
class BusinessLogicException(BaseAppException): class BusinessLogicException(BaseAppException):
@@ -175,15 +155,10 @@ class BusinessLogicException(BaseAppException):
def __init__( def __init__(
self, self,
message: str, message: str,
code: int = BusinessCode.OPERATION_NOT_ALLOWED, code: BusinessCode = BusinessCode.OPERATION_NOT_ALLOWED,
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
super().__init__( super().__init__(message=message, code=code, status_code=400, details=details)
message=message,
code=code,
status_code=400,
details=details
)
class DatabaseException(BaseAppException): class DatabaseException(BaseAppException):
@@ -197,13 +172,13 @@ class DatabaseException(BaseAppException):
def __init__( def __init__(
self, self,
message: str = "Database error occurred", message: str = "Database error occurred",
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
super().__init__( super().__init__(
message=message, message=message,
code=BusinessCode.DATABASE_ERROR, code=BusinessCode.DATABASE_ERROR,
status_code=503, status_code=503,
details=details details=details,
) )
@@ -218,13 +193,13 @@ class CacheException(BaseAppException):
def __init__( def __init__(
self, self,
message: str = "Cache service error", message: str = "Cache service error",
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
super().__init__( super().__init__(
message=message, message=message,
code=BusinessCode.CACHE_ERROR, code=BusinessCode.CACHE_ERROR,
status_code=503, status_code=503,
details=details details=details,
) )
@@ -239,11 +214,33 @@ class ExternalServiceException(BaseAppException):
def __init__( def __init__(
self, self,
message: str = "External service unavailable", message: str = "External service unavailable",
details: Optional[dict[str, Any]] = None details: Optional[dict[str, Any]] = None,
): ):
super().__init__( super().__init__(
message=message, message=message,
code=BusinessCode.EXTERNAL_SERVICE_ERROR, code=BusinessCode.EXTERNAL_SERVICE_ERROR,
status_code=502, 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,
) )

View File

@@ -6,7 +6,7 @@ Provides unified response structure for all API endpoints.
from typing import TypeVar, Generic, Optional, Any from typing import TypeVar, Generic, Optional, Any
from datetime import datetime from datetime import datetime
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from enum import IntEnum
T = TypeVar("T") T = TypeVar("T")
@@ -29,18 +29,9 @@ class ApiResponse(BaseModel, Generic[T]):
message: str = Field(description="Human-readable message") message: str = Field(description="Human-readable message")
data: Optional[T] = Field(default=None, description="Response payload") data: Optional[T] = Field(default=None, description="Response payload")
trace_id: str = Field(description="Request trace ID") trace_id: str = Field(description="Request trace ID")
timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response timestamp") 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"
}
}
class PaginationMeta(BaseModel): class PaginationMeta(BaseModel):
@@ -66,9 +57,7 @@ class PaginatedData(BaseModel, Generic[T]):
def success( def success(
data: Optional[T] = None, data: Optional[T] = None, message: str = "Success", trace_id: str = ""
message: str = "Success",
trace_id: str = ""
) -> ApiResponse[T]: ) -> ApiResponse[T]:
""" """
Create success response. Create success response.
@@ -85,19 +74,15 @@ def success(
return success(data={"user_id": 123}, message="User created") return success(data={"user_id": 123}, message="User created")
""" """
return ApiResponse( return ApiResponse(
code=0, code=BusinessCode.SUCCESS,
message=message, message=message,
data=data, data=data,
trace_id=trace_id, trace_id=trace_id,
timestamp=datetime.utcnow() timestamp=datetime.now(),
) )
def error( def error(code: "BusinessCode", data: Any=None, message: str="", trace_id: str = "") -> ApiResponse[None]:
code: int,
message: str,
trace_id: str = ""
) -> ApiResponse[None]:
""" """
Create error response. Create error response.
@@ -107,7 +92,7 @@ def error(
trace_id: Request trace ID trace_id: Request trace ID
Returns: Returns:
ApiResponse: Error response with data=None ApiResponse: Error response
Example: Example:
return error(code=1001, message="Login failed: Invalid credentials") return error(code=1001, message="Login failed: Invalid credentials")
@@ -115,12 +100,15 @@ def error(
if code <= 0: if code <= 0:
raise ValueError("Error code must be greater than 0") raise ValueError("Error code must be greater than 0")
if message == "":
message = BusinessCode(code).name
return ApiResponse( return ApiResponse(
code=code, code=code,
message=message, message=message,
data=None, data=data,
trace_id=trace_id, trace_id=trace_id,
timestamp=datetime.utcnow() timestamp=datetime.now(),
) )
@@ -130,7 +118,7 @@ def paginated(
page: int, page: int,
page_size: int, page_size: int,
message: str = "Success", message: str = "Success",
trace_id: str = "" trace_id: str = "",
) -> ApiResponse[PaginatedData[T]]: ) -> ApiResponse[PaginatedData[T]]:
""" """
Create paginated response. Create paginated response.
@@ -157,28 +145,22 @@ def paginated(
total_pages = (total + page_size - 1) // page_size if page_size > 0 else 0 total_pages = (total + page_size - 1) // page_size if page_size > 0 else 0
pagination_meta = PaginationMeta( pagination_meta = PaginationMeta(
total=total, total=total, page=page, page_size=page_size, total_pages=total_pages
page=page,
page_size=page_size,
total_pages=total_pages
) )
paginated_data = PaginatedData( paginated_data = PaginatedData(items=items, pagination=pagination_meta)
items=items,
pagination=pagination_meta
)
return ApiResponse( return ApiResponse(
code=0, code=0,
message=message, message=message,
data=paginated_data, data=paginated_data,
trace_id=trace_id, trace_id=trace_id,
timestamp=datetime.utcnow() timestamp=datetime.utcnow(),
) )
# Business code constants # Business code constants
class BusinessCode: class BusinessCode(IntEnum):
""" """
Business status code definitions. Business status code definitions.
@@ -221,10 +203,23 @@ class BusinessCode:
DATABASE_ERROR = 5001 DATABASE_ERROR = 5001
EXTERNAL_SERVICE_ERROR = 5002 EXTERNAL_SERVICE_ERROR = 5002
CACHE_ERROR = 5003 CACHE_ERROR = 5003
INTERNAL_ERROR = 5004
# 没有实现
NOT_IMPLEMENTED = 5004
# Unknown Errors (9000-9999) # Unknown Errors (9000-9999)
UNKNOWN_ERROR = 9000 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 # Common error response examples for OpenAPI documentation
ERROR_RESPONSES = { ERROR_RESPONSES = {
@@ -237,10 +232,10 @@ ERROR_RESPONSES = {
"message": "Validation error: email - field required", "message": "Validation error: email - field required",
"data": None, "data": None,
"trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2024-01-15T10:30:00Z" "timestamp": "2024-01-15T10:30:00Z",
} }
} }
} },
}, },
401: { 401: {
"description": "Unauthorized - Authentication failed", "description": "Unauthorized - Authentication failed",
@@ -251,10 +246,10 @@ ERROR_RESPONSES = {
"message": "Login failed: Invalid credentials", "message": "Login failed: Invalid credentials",
"data": None, "data": None,
"trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2024-01-15T10:30:00Z" "timestamp": "2024-01-15T10:30:00Z",
} }
} }
} },
}, },
403: { 403: {
"description": "Forbidden - Insufficient permissions", "description": "Forbidden - Insufficient permissions",
@@ -265,10 +260,10 @@ ERROR_RESPONSES = {
"message": "Insufficient permissions to perform this action", "message": "Insufficient permissions to perform this action",
"data": None, "data": None,
"trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2024-01-15T10:30:00Z" "timestamp": "2024-01-15T10:30:00Z",
} }
} }
} },
}, },
404: { 404: {
"description": "Not Found - Resource does not exist", "description": "Not Found - Resource does not exist",
@@ -279,10 +274,10 @@ ERROR_RESPONSES = {
"message": "User not found", "message": "User not found",
"data": None, "data": None,
"trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2024-01-15T10:30:00Z" "timestamp": "2024-01-15T10:30:00Z",
} }
} }
} },
}, },
409: { 409: {
"description": "Conflict - Resource already exists or conflicts", "description": "Conflict - Resource already exists or conflicts",
@@ -293,10 +288,10 @@ ERROR_RESPONSES = {
"message": "User already exists", "message": "User already exists",
"data": None, "data": None,
"trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2024-01-15T10:30:00Z" "timestamp": "2024-01-15T10:30:00Z",
} }
} }
} },
}, },
422: { 422: {
"description": "Unprocessable Entity - Validation error", "description": "Unprocessable Entity - Validation error",
@@ -307,10 +302,10 @@ ERROR_RESPONSES = {
"message": "Validation error: body -> email - value is not a valid email address", "message": "Validation error: body -> email - value is not a valid email address",
"data": None, "data": None,
"trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2024-01-15T10:30:00Z" "timestamp": "2024-01-15T10:30:00Z",
} }
} }
} },
}, },
500: { 500: {
"description": "Internal Server Error - Unexpected error", "description": "Internal Server Error - Unexpected error",
@@ -321,10 +316,10 @@ ERROR_RESPONSES = {
"message": "An unexpected error occurred", "message": "An unexpected error occurred",
"data": None, "data": None,
"trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2024-01-15T10:30:00Z" "timestamp": "2024-01-15T10:30:00Z",
} }
} }
} },
}, },
502: { 502: {
"description": "Bad Gateway - External service error", "description": "Bad Gateway - External service error",
@@ -335,10 +330,10 @@ ERROR_RESPONSES = {
"message": "External service unavailable", "message": "External service unavailable",
"data": None, "data": None,
"trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2024-01-15T10:30:00Z" "timestamp": "2024-01-15T10:30:00Z",
} }
} }
} },
}, },
503: { 503: {
"description": "Service Unavailable - Database or cache error", "description": "Service Unavailable - Database or cache error",
@@ -349,10 +344,10 @@ ERROR_RESPONSES = {
"message": "Database error occurred", "message": "Database error occurred",
"data": None, "data": None,
"trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"timestamp": "2024-01-15T10:30:00Z" "timestamp": "2024-01-15T10:30:00Z",
} }
} }
} },
}, },
} }

23
main.py
View File

@@ -4,7 +4,7 @@ Bootstraps the application with middleware, routers, and lifecycle handlers.
""" """
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import APIRouter, FastAPI
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from core.config import settings 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.trace_context import TraceContextMiddleware
from middleware.logging import RequestLoggingMiddleware from middleware.logging import RequestLoggingMiddleware
from middleware.error_handler import register_exception_handlers 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__) logger = get_logger(__name__)
@@ -40,7 +42,9 @@ async def lifespan(app: FastAPI):
if settings.otel_enabled: if settings.otel_enabled:
init_tracing() init_tracing()
instrument_app(app) 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 # Initialize Redis
await init_redis() await init_redis()
@@ -104,10 +108,9 @@ app.add_middleware(TraceContextMiddleware)
# Register exception handlers # Register exception handlers
register_exception_handlers(app) register_exception_handlers(app)
# Include application routers router = APIRouter(prefix="/api")
app.include_router(app_a_router) router.include_router(jd_router())
app.include_router(app_b_router) app.include_router(router)
# Health check endpoint # Health check endpoint
@app.get( @app.get(
@@ -119,7 +122,7 @@ app.include_router(app_b_router)
200: {"description": "Service is healthy"}, 200: {"description": "Service is healthy"},
500: ERROR_RESPONSES[500], 500: ERROR_RESPONSES[500],
503: ERROR_RESPONSES[503], 503: ERROR_RESPONSES[503],
} },
) )
async def health_check(): async def health_check():
""" """
@@ -159,7 +162,7 @@ async def health_check():
description="Get API information", description="Get API information",
responses={ responses={
200: {"description": "API information retrieved successfully"}, 200: {"description": "API information retrieved successfully"},
} },
) )
async def root(): async def root():
"""Root endpoint with API information.""" """Root endpoint with API information."""

View File

@@ -32,6 +32,12 @@ dependencies = [
"brotli>=1.1.0", "brotli>=1.1.0",
"psutil>=5.9.0", "psutil>=5.9.0",
"pycryptodome>=3.21.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] [project.optional-dependencies]

398
uv.lock generated
View File

@@ -1,6 +1,11 @@
version = 1 version = 1
revision = 2 revision = 2
requires-python = ">=3.13" 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]] [[package]]
name = "aiomysql" 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" }, { 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]] [[package]]
name = "certifi" name = "certifi"
version = "2025.10.5" 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" }, { 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]] [[package]]
name = "coverage" name = "coverage"
version = "7.11.0" 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" }, { 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]] [[package]]
name = "dnspython" name = "dnspython"
version = "2.8.0" 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" }, { 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]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.120.0" 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" }, { 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]] [[package]]
name = "googleapis-common-protos" name = "googleapis-common-protos"
version = "1.71.0" 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" }, { 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]] [[package]]
name = "idna" name = "idna"
version = "3.11" version = "3.11"
@@ -472,8 +575,12 @@ source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiomysql" }, { name = "aiomysql" },
{ name = "alembic" }, { name = "alembic" },
{ name = "brotli" },
{ name = "cryptography" }, { name = "cryptography" },
{ name = "curl-cffi" },
{ name = "ddddocr" },
{ name = "email-validator" }, { name = "email-validator" },
{ name = "fake-useragent" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "greenlet" }, { name = "greenlet" },
{ name = "gunicorn" }, { name = "gunicorn" },
@@ -483,16 +590,21 @@ dependencies = [
{ name = "opentelemetry-instrumentation-fastapi" }, { name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-instrumentation-httpx" }, { name = "opentelemetry-instrumentation-httpx" },
{ name = "opentelemetry-instrumentation-redis" }, { name = "opentelemetry-instrumentation-redis" },
{ name = "opentelemetry-instrumentation-requests" },
{ name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "opentelemetry-instrumentation-sqlalchemy" },
{ name = "opentelemetry-sdk" }, { name = "opentelemetry-sdk" },
{ name = "orjson" }, { name = "orjson" },
{ name = "psutil" },
{ name = "pycryptodome" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "pyexecjs" },
{ name = "pymysql" }, { name = "pymysql" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "redis" }, { name = "redis" },
{ name = "sqlmodel" }, { name = "sqlmodel" },
{ name = "tenacity" },
{ name = "uvicorn", extra = ["standard"] }, { name = "uvicorn", extra = ["standard"] },
] ]
@@ -511,8 +623,12 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "aiomysql", specifier = ">=0.2.0" }, { name = "aiomysql", specifier = ">=0.2.0" },
{ name = "alembic", specifier = ">=1.14.0" }, { name = "alembic", specifier = ">=1.14.0" },
{ name = "brotli", specifier = ">=1.1.0" },
{ name = "cryptography", specifier = ">=46.0.1" }, { 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 = "email-validator", specifier = ">=2.3.0" },
{ name = "fake-useragent", specifier = ">=2.2.0" },
{ name = "fastapi", specifier = ">=0.120.0" }, { name = "fastapi", specifier = ">=0.120.0" },
{ name = "greenlet", specifier = ">=3.2.4" }, { name = "greenlet", specifier = ">=3.2.4" },
{ name = "gunicorn", specifier = ">=23.0.0" }, { name = "gunicorn", specifier = ">=23.0.0" },
@@ -524,11 +640,15 @@ requires-dist = [
{ name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.59b0" }, { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.59b0" },
{ name = "opentelemetry-instrumentation-httpx", specifier = ">=0.59b0" }, { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.59b0" },
{ name = "opentelemetry-instrumentation-redis", 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-instrumentation-sqlalchemy", specifier = ">=0.59b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.38.0" }, { name = "opentelemetry-sdk", specifier = ">=1.38.0" },
{ name = "orjson", specifier = ">=3.11.4" }, { 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", specifier = ">=2.12.3" },
{ name = "pydantic-settings", specifier = ">=2.11.0" }, { name = "pydantic-settings", specifier = ">=2.11.0" },
{ name = "pyexecjs", specifier = ">=1.5.1" },
{ name = "pymysql", specifier = ">=1.1.1" }, { name = "pymysql", specifier = ">=1.1.1" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.4" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.4" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
@@ -539,6 +659,7 @@ requires-dist = [
{ name = "redis", specifier = ">=5.2.1" }, { name = "redis", specifier = ">=5.2.1" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.4" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.4" },
{ name = "sqlmodel", specifier = ">=0.0.24" }, { name = "sqlmodel", specifier = ">=0.0.24" },
{ name = "tenacity", specifier = ">=9.1.2" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.38.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.38.0" },
] ]
provides-extras = ["dev"] 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" }, { 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]] [[package]]
name = "mypy" name = "mypy"
version = "1.18.2" 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" }, { 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]] [[package]]
name = "opentelemetry-api" name = "opentelemetry-api"
version = "1.38.0" 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" }, { 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]] [[package]]
name = "opentelemetry-instrumentation-sqlalchemy" name = "opentelemetry-instrumentation-sqlalchemy"
version = "0.59b0" 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" }, { 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]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.6.0" 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" }, { 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]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.23" 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" }, { 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]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.3" 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" }, { 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]] [[package]]
name = "pygments" name = "pygments"
version = "2.19.2" 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" }, { 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]] [[package]]
name = "pytest" name = "pytest"
version = "8.4.2" 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" }, { 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]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.1" 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" }, { 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]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"