接入 OTel 链路追踪 + 镜像默认启用

抓取-解析链路原本只有日志,出现"抓回内容但解析不出预期字段"时定位慢。
接入 OpenTelemetry traces(FastAPI/httpx 自动 + 手写 fetch/parse span),
解析失败时把页面 HTML 作为 span event 上报,便于事后复现。
Dockerfile 默认开启,镜像一启动即导出到自建 OTLP endpoint。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 15:43:19 +08:00
co-authored by Claude Opus 4.6
parent 5376ced511
commit 2b7db521c4
13 changed files with 1021 additions and 203 deletions
+13
View File
@@ -63,3 +63,16 @@ RAKUTEN_AUTH_STATE_DIR=.auth
# 下单金额上限(日元):实际应付超过该值直接拒绝提交,防止解析出错或页面改版 # 下单金额上限(日元):实际应付超过该值直接拒绝提交,防止解析出错或页面改版
# 导致买到远超预期的订单。设为 0 表示不设上限(不建议)。 # 导致买到远超预期的订单。设为 0 表示不设上限(不建议)。
RAKUTEN_ORDER_MAX_TOTAL_YEN=30000 RAKUTEN_ORDER_MAX_TOTAL_YEN=30000
# ---- OpenTelemetry traces(可选;默认关闭)----
# 启用后把抓取-解析链路以 span 导出到 OTLP/HTTP endpoint,
# 用于排查"抓到的内容为什么解析不出预期字段"。
# 解析失败时会把页面 HTML 作为 span event 上报(上限可配),便于事后复现。
RAKUTEN_OTEL_ENABLED=false
RAKUTEN_OTEL_ENDPOINT=https://oltp.jerryyan.top/v1/traces
# 服务名按服务覆盖;不设时两侧 main.py 分别按 rakuten-scraping / rakuten-trading 落地
RAKUTEN_OTEL_SERVICE_NAME=
# OTLP 鉴权头(形如 key=val,key=val);当前 endpoint 裸跑,留空
RAKUTEN_OTEL_HEADERS=
# 失败 HTML 快照上限字节(默认 2MB,超出截断并标注 truncated=true)
RAKUTEN_OTEL_SNAPSHOT_MAX_BYTES=2000000
+11 -1
View File
@@ -7,6 +7,11 @@
# 镜像默认包含 Playwright 浏览器兜底(与 .env.example 的 # 镜像默认包含 Playwright 浏览器兜底(与 .env.example 的
# RAKUTEN_BROWSER_FALLBACK_ENABLED=true 对齐),首次构建多约 400MB。 # RAKUTEN_BROWSER_FALLBACK_ENABLED=true 对齐),首次构建多约 400MB。
# 若要裁剪,可改为 uv sync --no-dev(去掉 --extra browser)。 # 若要裁剪,可改为 uv sync --no-dev(去掉 --extra browser)。
#
# 镜像默认开启 OpenTelemetry traces:RAKUTEN_OTEL_ENABLED=true +
# RAKUTEN_OTEL_ENDPOINT=https://oltp.jerryyan.top/v1/traces 写入 ENV。
# 容器一启动就把抓取-解析链路导出到自建 OTLP 后端,便于排查"抓回的内容
# 为什么解析不出预期字段"。endpoint 裸跑(无鉴权);需要时运行时覆盖。
# ----------------------------- builder ----------------------------- # ----------------------------- builder -----------------------------
FROM registry.jerryyan.top/library/python:3.13-slim AS builder FROM registry.jerryyan.top/library/python:3.13-slim AS builder
@@ -46,7 +51,12 @@ ENV PYTHONUNBUFFERED=1 \
# Playwright 浏览器二进制走 npmmirror 镜像,避免从 azureedge 拉取缓慢 # Playwright 浏览器二进制走 npmmirror 镜像,避免从 azureedge 拉取缓慢
PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright \ PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright \
RAKUTEN_APP_HOST=0.0.0.0 \ RAKUTEN_APP_HOST=0.0.0.0 \
RAKUTEN_APP_PORT=31107 RAKUTEN_APP_PORT=31107 \
# OpenTelemetry traces 默认开启:镜像一启动就把抓取-解析链路导出到自建 OTLP endpoint。
# endpoint 裸跑(无鉴权);如部署到需要鉴权的网关,运行时覆盖 RAKUTEN_OTEL_HEADERS 即可。
# 服务名(rakuten-scraping / rakuten-trading)由两侧 main.py 显式设定,此处不覆盖。
RAKUTEN_OTEL_ENABLED=true \
RAKUTEN_OTEL_ENDPOINT=https://oltp.jerryyan.top/v1/traces
# Chromium 运行所需的最小系统依赖。 # Chromium 运行所需的最小系统依赖。
# 这里显式列出而不是用 `playwright install-deps`,是因为后者要求 root + apt # 这里显式列出而不是用 `playwright install-deps`,是因为后者要求 root + apt
+6
View File
@@ -27,6 +27,7 @@ from app.scraping.services.site_session import SiteSession
from app.shared.api import register_exception_handlers from app.shared.api import register_exception_handlers
from app.shared.config import get_settings from app.shared.config import get_settings
from app.shared.logging_setup import configure_logging from app.shared.logging_setup import configure_logging
from app.shared.telemetry import instrument_app, setup_telemetry, shutdown_telemetry
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -56,6 +57,9 @@ async def lifespan(app: FastAPI):
app.state.container = container app.state.container = container
configure_logging(container.settings) configure_logging(container.settings)
# telemetry 必须在创建任何 httpx.AsyncClient 之前完成 setup,否则 httpx
# 自动 instrumentation 抓不到现有客户端的请求。
setup_telemetry(container.settings, service_name="rakuten-scraping")
logger.info("抓取服务启动:%s:%s", container.settings.app_host, container.settings.app_port) logger.info("抓取服务启动:%s:%s", container.settings.app_host, container.settings.app_port)
logger.info("日志级别:%s", container.settings.log_level) logger.info("日志级别:%s", container.settings.log_level)
logger.info("当前环境:%s", container.settings.app_env) logger.info("当前环境:%s", container.settings.app_env)
@@ -67,6 +71,7 @@ async def lifespan(app: FastAPI):
await container.rakuma_session.close() await container.rakuma_session.close()
await container.site_session.close() await container.site_session.close()
await container.browser_fallback.close() await container.browser_fallback.close()
shutdown_telemetry()
def create_app() -> FastAPI: def create_app() -> FastAPI:
@@ -76,6 +81,7 @@ def create_app() -> FastAPI:
app.include_router(scrape_router) app.include_router(scrape_router)
app.include_router(rakuma_router) app.include_router(rakuma_router)
register_exception_handlers(app) register_exception_handlers(app)
instrument_app(app)
return app return app
+64
View File
@@ -8,6 +8,8 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
from opentelemetry import trace
from app.scraping.core import rakuma_site as site from app.scraping.core import rakuma_site as site
from app.shared.config import Settings from app.shared.config import Settings
from app.scraping.models.scrape import ( from app.scraping.models.scrape import (
@@ -35,8 +37,10 @@ from app.scraping.utils.rakuma_urls import (
split_item_url, split_item_url,
split_shop_url, split_shop_url,
) )
from app.shared.telemetry import snapshot
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
tracer = trace.get_tracer(__name__)
class RakumaClient: class RakumaClient:
@@ -54,6 +58,11 @@ class RakumaClient:
url = build_search_url(payload) url = build_search_url(payload)
logger.info("抓取 ラクマ 搜索页:url=%s", url) logger.info("抓取 ラクマ 搜索页:url=%s", url)
html: str | None = None
with tracer.start_as_current_span("parse.rakuma.search") as span:
span.set_attribute("parse.site", "rakuma")
span.set_attribute("parse.op", "search")
try:
html = await self._session.fetch_html(url) html = await self._session.fetch_html(url)
result = parse_search( result = parse_search(
html, html,
@@ -61,11 +70,18 @@ class RakumaClient:
page=payload.page, page=payload.page,
keyword=payload.keyword.strip(), keyword=payload.keyword.strip(),
) )
span.set_attribute("parse.items", len(result.items))
span.set_attribute("parse.total", result.total_count)
logger.info( logger.info(
"ラクマ 搜索完成:url=%s items=%s total=%s", "ラクマ 搜索完成:url=%s items=%s total=%s",
url, len(result.items), result.total_count, url, len(result.items), result.total_count,
) )
return result return result
except Exception:
span.record_exception()
span.set_attribute("parse.fail_reason", "parse_error")
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
raise
async def categories(self, payload: RakumaCategoryRequest) -> RakumaCategoryData: async def categories(self, payload: RakumaCategoryRequest) -> RakumaCategoryData:
"""抓取分类树 """抓取分类树
@@ -76,17 +92,29 @@ class RakumaClient:
url = site.CATEGORY_LIST_URL url = site.CATEGORY_LIST_URL
logger.info("抓取 ラクマ 分类页:category_id=%s", payload.category_id) logger.info("抓取 ラクマ 分类页:category_id=%s", payload.category_id)
html: str | None = None
with tracer.start_as_current_span("parse.rakuma.categories") as span:
span.set_attribute("parse.site", "rakuma")
span.set_attribute("parse.op", "categories")
try:
html = await self._session.fetch_html(url) html = await self._session.fetch_html(url)
data = parse_categories( data = parse_categories(
html, html,
category_id=payload.category_id, category_id=payload.category_id,
include_descendants=payload.include_descendants, include_descendants=payload.include_descendants,
) )
span.set_attribute("parse.children", len(data.children))
span.set_attribute("parse.tree", data.total_count)
logger.info( logger.info(
"ラクマ 分类完成:category_id=%s name=%s children=%s tree=%s", "ラクマ 分类完成:category_id=%s name=%s children=%s tree=%s",
data.category_id, data.name, len(data.children), data.total_count, data.category_id, data.name, len(data.children), data.total_count,
) )
return data return data
except Exception:
span.record_exception()
span.set_attribute("parse.fail_reason", "parse_error")
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
raise
async def item_detail(self, payload: RakumaItemDetailRequest) -> RakumaItemDetailData: async def item_detail(self, payload: RakumaItemDetailRequest) -> RakumaItemDetailData:
"""抓取商品详情""" """抓取商品详情"""
@@ -97,13 +125,25 @@ class RakumaClient:
url = build_item_url(item_id) url = build_item_url(item_id)
logger.info("抓取 ラクマ 商品详情:url=%s", url) logger.info("抓取 ラクマ 商品详情:url=%s", url)
html: str | None = None
with tracer.start_as_current_span("parse.rakuma.item_detail") as span:
span.set_attribute("parse.site", "rakuma")
span.set_attribute("parse.op", "item_detail")
try:
html = await self._session.fetch_html(url) html = await self._session.fetch_html(url)
detail = parse_item_detail(html, item_id=item_id, item_url=url) detail = parse_item_detail(html, item_id=item_id, item_url=url)
span.set_attribute("parse.price", detail.price)
span.set_attribute("parse.sold_out", detail.is_sold_out)
logger.info( logger.info(
"ラクマ 详情完成:url=%s name=%s price=%s sold_out=%s", "ラクマ 详情完成:url=%s name=%s price=%s sold_out=%s",
url, detail.item_name[:40], detail.price, detail.is_sold_out, url, detail.item_name[:40], detail.price, detail.is_sold_out,
) )
return detail return detail
except Exception:
span.record_exception()
span.set_attribute("parse.fail_reason", "parse_error")
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
raise
async def shop_detail(self, payload: RakumaShopDetailRequest) -> RakumaShopDetailData: async def shop_detail(self, payload: RakumaShopDetailRequest) -> RakumaShopDetailData:
"""抓取卖家详情 """抓取卖家详情
@@ -114,6 +154,11 @@ class RakumaClient:
url = build_shop_url(shop_id) url = build_shop_url(shop_id)
logger.info("抓取 ラクマ 卖家详情:url=%s reviews=%s", url, payload.include_reviews) logger.info("抓取 ラクマ 卖家详情:url=%s reviews=%s", url, payload.include_reviews)
html: str | None = None
with tracer.start_as_current_span("parse.rakuma.shop_detail") as span:
span.set_attribute("parse.site", "rakuma")
span.set_attribute("parse.op", "shop_detail")
try:
if payload.include_reviews: if payload.include_reviews:
html, review_html = await asyncio.gather( html, review_html = await asyncio.gather(
self._session.fetch_html(url), self._session.fetch_html(url),
@@ -125,11 +170,18 @@ class RakumaClient:
detail = parse_shop_detail( detail = parse_shop_detail(
html, shop_id=shop_id, shop_url=url, review_html=review_html html, shop_id=shop_id, shop_url=url, review_html=review_html
) )
span.set_attribute("parse.items", detail.item_count)
span.set_attribute("parse.reviews", detail.review_count)
logger.info( logger.info(
"ラクマ 卖家详情完成:shop_id=%s name=%s items=%s reviews=%s", "ラクマ 卖家详情完成:shop_id=%s name=%s items=%s reviews=%s",
shop_id, detail.shop_name, detail.item_count, detail.review_count, shop_id, detail.shop_name, detail.item_count, detail.review_count,
) )
return detail return detail
except Exception:
span.record_exception()
span.set_attribute("parse.fail_reason", "parse_error")
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
raise
async def shop_items(self, payload: RakumaShopItemsRequest) -> RakumaShopItemsData: async def shop_items(self, payload: RakumaShopItemsRequest) -> RakumaShopItemsData:
"""抓取卖家的商品列表""" """抓取卖家的商品列表"""
@@ -137,15 +189,27 @@ class RakumaClient:
url = build_shop_url(shop_id, page=payload.page) url = build_shop_url(shop_id, page=payload.page)
logger.info("抓取 ラクマ 卖家商品:url=%s", url) logger.info("抓取 ラクマ 卖家商品:url=%s", url)
html: str | None = None
with tracer.start_as_current_span("parse.rakuma.shop_items") as span:
span.set_attribute("parse.site", "rakuma")
span.set_attribute("parse.op", "shop_items")
try:
html = await self._session.fetch_html(url) html = await self._session.fetch_html(url)
result = parse_shop_items( result = parse_shop_items(
html, shop_id=shop_id, request_url=url, page=payload.page html, shop_id=shop_id, request_url=url, page=payload.page
) )
span.set_attribute("parse.items", len(result.items))
span.set_attribute("parse.total", result.total_count)
logger.info( logger.info(
"ラクマ 卖家商品完成:shop_id=%s items=%s total=%s", "ラクマ 卖家商品完成:shop_id=%s items=%s total=%s",
shop_id, len(result.items), result.total_count, shop_id, len(result.items), result.total_count,
) )
return result return result
except Exception:
span.record_exception()
span.set_attribute("parse.fail_reason", "parse_error")
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
raise
@staticmethod @staticmethod
def _resolve_shop_id(shop_url: object, shop_id: str | None) -> str: def _resolve_shop_id(shop_url: object, shop_id: str | None) -> str:
+30 -4
View File
@@ -18,6 +18,7 @@ import logging
from typing import Any from typing import Any
import httpx import httpx
from opentelemetry import trace
from app.scraping.core import rakuma_site as site from app.scraping.core import rakuma_site as site
from app.shared.config import Settings from app.shared.config import Settings
@@ -27,6 +28,7 @@ from app.shared.errors import (
UpstreamBlockedError, UpstreamBlockedError,
UpstreamRequestError, UpstreamRequestError,
) )
from app.shared.telemetry import snapshot
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -89,8 +91,14 @@ class RakumaSession:
if client is None: if client is None:
raise UpstreamRequestError("ラクマ 会话尚未初始化") raise UpstreamRequestError("ラクマ 会话尚未初始化")
tracer = trace.get_tracer(__name__)
max_attempts = max(1, self._settings.http_max_attempts) max_attempts = max(1, self._settings.http_max_attempts)
last_error = "unknown error" last_error = "unknown error"
last_text: str | None = None
with tracer.start_as_current_span("scrape.fetch") as span:
span.set_attribute("scrape.url", url)
span.set_attribute("scrape.profile", "rakuma")
try: try:
await asyncio.wait_for( await asyncio.wait_for(
@@ -98,10 +106,14 @@ class RakumaSession:
timeout=self._settings.request_timeout_seconds, timeout=self._settings.request_timeout_seconds,
) )
except TimeoutError as exc: except TimeoutError as exc:
raise ResourceBusyError() from exc err = ResourceBusyError()
span.record_exception(err)
span.set_attribute("scrape.fail_reason", "ResourceBusyError")
raise err from exc
try: try:
for attempt in range(1, max_attempts + 1): for attempt in range(1, max_attempts + 1):
span.set_attribute("scrape.attempts", attempt)
try: try:
response = await client.get(url) response = await client.get(url)
except httpx.HTTPError as exc: except httpx.HTTPError as exc:
@@ -113,9 +125,15 @@ class RakumaSession:
continue continue
if response.status_code == 404: if response.status_code == 404:
raise ItemNotFoundError(f"Page not found: {url}") err = ItemNotFoundError(f"Page not found: {url}")
span.record_exception(err)
span.set_attribute("scrape.fail_reason", "ItemNotFoundError")
raise err
if response.status_code < 400: if response.status_code < 400:
span.set_attribute("scrape.last_status_code", response.status_code)
span.set_attribute("scrape.final_url", str(response.url))
span.set_attribute("scrape.html_bytes", len(response.text))
return response.text return response.text
last_error = ( last_error = (
@@ -123,13 +141,21 @@ class RakumaSession:
if response.status_code >= 500 if response.status_code >= 500
else f"status {response.status_code}" else f"status {response.status_code}"
) )
last_text = response.text
span.set_attribute("scrape.last_status_code", response.status_code)
span.set_attribute("scrape.html_bytes", len(response.text))
logger.warning( logger.warning(
"ラクマ 抓取结果异常:url=%s attempt=%s/%s %s", "ラクマ 抓取结果异常:url=%s attempt=%s/%s %s",
url, attempt, max_attempts, last_error, url, attempt, max_attempts, last_error,
) )
if last_error.startswith("upstream status") or ":" in last_error: if last_error.startswith("upstream status") or ":" in last_error:
raise UpstreamRequestError(f"Upstream request failed: {last_error}") err = UpstreamRequestError(f"Upstream request failed: {last_error}")
raise UpstreamBlockedError(f"Failed to fetch {url}: {last_error}") else:
err = UpstreamBlockedError(f"Failed to fetch {url}: {last_error}")
span.record_exception(err)
span.set_attribute("scrape.fail_reason", type(err).__name__)
snapshot(span, "scrape.failed_html", last_text, self._settings.otel_snapshot_max_bytes)
raise err
finally: finally:
self._semaphore.release() self._semaphore.release()
+65 -1
View File
@@ -8,6 +8,8 @@ from __future__ import annotations
import logging import logging
from opentelemetry import trace
from app.scraping.core import site from app.scraping.core import site
from app.shared.config import Settings from app.shared.config import Settings
from app.shared.errors import ScrapeParseError from app.shared.errors import ScrapeParseError
@@ -43,8 +45,10 @@ from app.scraping.utils.urls import (
split_item_url, split_item_url,
split_shop_url, split_shop_url,
) )
from app.shared.telemetry import snapshot
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
tracer = trace.get_tracer(__name__)
class RakutenClient: class RakutenClient:
@@ -62,6 +66,11 @@ class RakutenClient:
url = build_search_url(payload) url = build_search_url(payload)
logger.info("抓取搜索页:url=%s", url) logger.info("抓取搜索页:url=%s", url)
html: str | None = None
with tracer.start_as_current_span("parse.rakuten.search") as span:
span.set_attribute("parse.site", "rakuten")
span.set_attribute("parse.op", "search")
try:
html = await self._session.fetch_html(url, mobile=False) html = await self._session.fetch_html(url, mobile=False)
state = extract_initial_state(html) state = extract_initial_state(html)
result = parse_search( result = parse_search(
@@ -70,11 +79,19 @@ class RakutenClient:
page=payload.page, page=payload.page,
exclude_ads=payload.exclude_ads, exclude_ads=payload.exclude_ads,
) )
span.set_attribute("parse.items", len(result.items))
span.set_attribute("parse.ads", result.ad_count)
span.set_attribute("parse.total", result.total_count)
logger.info( logger.info(
"搜索完成:url=%s items=%s ads=%s total=%s", "搜索完成:url=%s items=%s ads=%s total=%s",
url, len(result.items), result.ad_count, result.total_count, url, len(result.items), result.ad_count, result.total_count,
) )
return result return result
except Exception:
span.record_exception()
span.set_attribute("parse.fail_reason", "parse_error")
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
raise
async def genres(self, payload: GenreRequest) -> GenreData: async def genres(self, payload: GenreRequest) -> GenreData:
"""抓取分类树:顶层分类列表,或指定分类的信息与直接子分类""" """抓取分类树:顶层分类列表,或指定分类的信息与直接子分类"""
@@ -82,14 +99,25 @@ class RakutenClient:
url = build_genre_url(genre_id) url = build_genre_url(genre_id)
logger.info("抓取分类树:url=%s genre_id=%s", url, genre_id) logger.info("抓取分类树:url=%s genre_id=%s", url, genre_id)
html: str | None = None
with tracer.start_as_current_span("parse.rakuten.genres") as span:
span.set_attribute("parse.site", "rakuten")
span.set_attribute("parse.op", "genres")
try:
html = await self._session.fetch_html(url, mobile=False) html = await self._session.fetch_html(url, mobile=False)
state = extract_initial_state(html) state = extract_initial_state(html)
result = parse_genres(state, genre_id=genre_id) result = parse_genres(state, genre_id=genre_id)
span.set_attribute("parse.children", len(result.children))
logger.info( logger.info(
"分类树完成:genre_id=%s name=%s children=%s", "分类树完成:genre_id=%s name=%s children=%s",
result.genre_id or "root", result.name, len(result.children), result.genre_id or "root", result.name, len(result.children),
) )
return result return result
except Exception:
span.record_exception()
span.set_attribute("parse.fail_reason", "parse_error")
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
raise
async def shop_detail(self, payload: ShopDetailRequest) -> ShopDetailData: async def shop_detail(self, payload: ShopDetailRequest) -> ShopDetailData:
"""抓取商家详情""" """抓取商家详情"""
@@ -100,14 +128,25 @@ class RakutenClient:
url = build_shop_url(shop_code) url = build_shop_url(shop_code)
logger.info("抓取店铺页:url=%s", url) logger.info("抓取店铺页:url=%s", url)
html: str | None = None
with tracer.start_as_current_span("parse.rakuten.shop_detail") as span:
span.set_attribute("parse.site", "rakuten")
span.set_attribute("parse.op", "shop_detail")
try:
html = await self._session.fetch_html(url, mobile=False) html = await self._session.fetch_html(url, mobile=False)
state = extract_initial_state(html) state = extract_initial_state(html)
result = parse_shop_detail(state, shop_code=shop_code, html=html) result = parse_shop_detail(state, shop_code=shop_code, html=html)
span.set_attribute("parse.reviews", result.review_count)
logger.info( logger.info(
"店铺详情完成:shop_code=%s shop_id=%s name=%s reviews=%s", "店铺详情完成:shop_code=%s shop_id=%s name=%s reviews=%s",
result.shop_code, result.shop_id, result.shop_name, result.review_count, result.shop_code, result.shop_id, result.shop_name, result.review_count,
) )
return result return result
except Exception:
span.record_exception()
span.set_attribute("parse.fail_reason", "parse_error")
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
raise
async def shop_items(self, payload: ShopItemsRequest) -> SearchResultData: async def shop_items(self, payload: ShopItemsRequest) -> SearchResultData:
"""抓取商家名下的商品列表 """抓取商家名下的商品列表
@@ -116,6 +155,10 @@ class RakutenClient:
`sid=` 限定店铺后的产物,因此这里转成一次搜索。只给 shop_code 时 `sid=` 限定店铺后的产物,因此这里转成一次搜索。只给 shop_code 时
需要先取一次店铺详情换出 shop_id。 需要先取一次店铺详情换出 shop_id。
""" """
with tracer.start_as_current_span("parse.rakuten.shop_items") as span:
span.set_attribute("parse.site", "rakuten")
span.set_attribute("parse.op", "shop_items")
try:
shop_id = payload.shop_id shop_id = payload.shop_id
if shop_id is None: if shop_id is None:
detail = await self.shop_detail(ShopDetailRequest(shop_code=payload.shop_code)) detail = await self.shop_detail(ShopDetailRequest(shop_code=payload.shop_code))
@@ -123,7 +166,14 @@ class RakutenClient:
raise ScrapeParseError(f"未能取得店铺 {payload.shop_code} 的 shop_id") raise ScrapeParseError(f"未能取得店铺 {payload.shop_code} 的 shop_id")
shop_id = detail.shop_id shop_id = detail.shop_id
return await self.search(payload.to_search_request(shop_id)) result = await self.search(payload.to_search_request(shop_id))
span.set_attribute("parse.items", len(result.items))
span.set_attribute("parse.total", result.total_count)
return result
except Exception:
span.record_exception()
span.set_attribute("parse.fail_reason", "parse_error")
raise
async def item_detail(self, payload: ItemDetailRequest) -> ItemDetailData: async def item_detail(self, payload: ItemDetailRequest) -> ItemDetailData:
"""抓取商品详情""" """抓取商品详情"""
@@ -137,11 +187,17 @@ class RakutenClient:
url = build_item_url(shop_code, item_code) url = build_item_url(shop_code, item_code)
logger.info("抓取商品详情:url=%s", url) logger.info("抓取商品详情:url=%s", url)
html: str | None = None
with tracer.start_as_current_span("parse.rakuten.item_detail") as span:
span.set_attribute("parse.site", "rakuten")
span.set_attribute("parse.op", "item_detail")
try:
# 部分官方旗舰店的商品页会跳出市场域名,落到各自的独立站点; # 部分官方旗舰店的商品页会跳出市场域名,落到各自的独立站点;
# 校验与解析都按最终落地域名分派,落到未登记站点时由校验器抛错。 # 校验与解析都按最终落地域名分派,落到未登记站点时由校验器抛错。
page = await self._session.fetch( page = await self._session.fetch(
url, mobile=True, validator=build_item_page_validator(url) url, mobile=True, validator=build_item_page_validator(url)
) )
html = page.html
if host_of(page.url) == site.ITEM_HOST: if host_of(page.url) == site.ITEM_HOST:
detail = parse_item_detail( detail = parse_item_detail(
@@ -162,8 +218,16 @@ class RakutenClient:
) )
) )
span.set_attribute("parse.source", detail.source)
span.set_attribute("parse.price", detail.price)
span.set_attribute("parse.variant_count", detail.sku.variant_count)
logger.info( logger.info(
"详情完成:url=%s source=%s name=%s price=%s skus=%s", "详情完成:url=%s source=%s name=%s price=%s skus=%s",
url, detail.source, detail.item_name[:40], detail.price, detail.sku.variant_count, url, detail.source, detail.item_name[:40], detail.price, detail.sku.variant_count,
) )
return detail return detail
except Exception:
span.record_exception()
span.set_attribute("parse.fail_reason", "parse_error")
snapshot(span, "parse.failed_html", html, self._settings.otel_snapshot_max_bytes)
raise
+34 -4
View File
@@ -19,6 +19,7 @@ from dataclasses import dataclass, field
from typing import Any from typing import Any
import httpx import httpx
from opentelemetry import trace
from app.scraping.core import site from app.scraping.core import site
from app.shared.config import Settings from app.shared.config import Settings
@@ -30,6 +31,7 @@ from app.shared.errors import (
) )
from app.scraping.parsers.state import PageValidator, require_state_marker from app.scraping.parsers.state import PageValidator, require_state_marker
from app.scraping.services.browser_fallback import BrowserFallback from app.scraping.services.browser_fallback import BrowserFallback
from app.shared.telemetry import snapshot
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -152,10 +154,17 @@ class SiteSession:
ResourceBusyError: 等待并发槽位超时 ResourceBusyError: 等待并发槽位超时
AppError: 校验器判定为不可重试的失败(如落到不支持的站点) AppError: 校验器判定为不可重试的失败(如落到不支持的站点)
""" """
tracer = trace.get_tracer(__name__)
profile = self._profiles["sp" if mobile else "pc"] profile = self._profiles["sp" if mobile else "pc"]
max_attempts = max(1, self._settings.http_max_attempts) max_attempts = max(1, self._settings.http_max_attempts)
validate = validator or require_state_marker validate = validator or require_state_marker
last_error: str = "unknown error" last_error: str = "unknown error"
# 失败时供 trace snapshot 上报:保留最后一次拿到的响应体(含挑战页 / 5xx 响应)。
last_text: str | None = None
with tracer.start_as_current_span("scrape.fetch") as span:
span.set_attribute("scrape.url", url)
span.set_attribute("scrape.profile", profile.name)
try: try:
await asyncio.wait_for( await asyncio.wait_for(
@@ -163,10 +172,14 @@ class SiteSession:
timeout=self._settings.request_timeout_seconds, timeout=self._settings.request_timeout_seconds,
) )
except TimeoutError as exc: except TimeoutError as exc:
raise ResourceBusyError() from exc err = ResourceBusyError()
span.record_exception(err)
span.set_attribute("scrape.fail_reason", "ResourceBusyError")
raise err from exc
try: try:
for attempt in range(1, max_attempts + 1): for attempt in range(1, max_attempts + 1):
span.set_attribute("scrape.attempts", attempt)
await self._ensure_warm(profile) await self._ensure_warm(profile)
try: try:
response = await profile.client.get(url) response = await profile.client.get(url)
@@ -179,18 +192,28 @@ class SiteSession:
continue continue
if response.status_code == 404: if response.status_code == 404:
raise ItemNotFoundError(f"Page not found: {url}") err = ItemNotFoundError(f"Page not found: {url}")
span.record_exception(err)
span.set_attribute("scrape.fail_reason", "ItemNotFoundError")
raise err
text = response.text text = response.text
final_url = str(response.url) final_url = str(response.url)
span.set_attribute("scrape.last_status_code", response.status_code)
span.set_attribute("scrape.final_url", final_url)
span.set_attribute("scrape.html_bytes", len(text))
if response.status_code < 400: if response.status_code < 400:
# 校验器可能直接抛 AppError 表示「重试也没用」,此处不拦截 # 校验器可能直接抛 AppError 表示「重试也没用」,此处不拦截
reason = validate(text, final_url) reason = validate(text, final_url)
if reason is None: if reason is None:
return FetchedPage(html=text, url=final_url) return FetchedPage(html=text, url=final_url)
last_error = self._refine_failure(reason, text) last_error = self._refine_failure(reason, text)
if last_error.startswith("challenge page detected"):
span.set_attribute("scrape.challenge_detected", True)
else: else:
last_error = self._describe_error_status(response.status_code) last_error = self._describe_error_status(response.status_code)
last_text = text
logger.warning( logger.warning(
"抓取结果异常:url=%s profile=%s attempt=%s/%s %s", "抓取结果异常:url=%s profile=%s attempt=%s/%s %s",
url, profile.name, attempt, max_attempts, last_error, url, profile.name, attempt, max_attempts, last_error,
@@ -205,11 +228,18 @@ class SiteSession:
else: else:
page = await self._escalate_to_browser(profile, url, validate) page = await self._escalate_to_browser(profile, url, validate)
if page is not None: if page is not None:
span.set_attribute("scrape.fell_back_to_browser", True)
span.set_attribute("scrape.final_url", page.url)
return page return page
if self._is_server_error(last_error): if self._is_server_error(last_error):
raise UpstreamRequestError(f"Upstream request failed: {last_error}") err = UpstreamRequestError(f"Upstream request failed: {last_error}")
raise UpstreamBlockedError(f"Blocked while fetching {url}: {last_error}") else:
err = UpstreamBlockedError(f"Blocked while fetching {url}: {last_error}")
span.record_exception(err)
span.set_attribute("scrape.fail_reason", type(err).__name__)
snapshot(span, "scrape.failed_html", last_text, self._settings.otel_snapshot_max_bytes)
raise err
finally: finally:
self._semaphore.release() self._semaphore.release()
+12
View File
@@ -82,6 +82,18 @@ class Settings(BaseSettings):
proxy_username: str | None = None proxy_username: str | None = None
proxy_password: str | None = None proxy_password: str | None = None
# ---- OpenTelemetry traces(通用,可选;默认关闭)----
# 启用后把抓取-解析链路以 span 导出到 OTLP/HTTP endpoint,用于排查"抓到
# 内容能否被解析"。两侧 main.py 在 lifespan 启动时按服务名分别初始化。
otel_enabled: bool = False
otel_endpoint: str | None = None # OTLP/HTTP traces endpoint,例如 https://oltp.jerryyan.top/v1/traces
otel_service_name: str = "rakuten" # 仅作兜底;实际值由两侧 main.py 显式覆盖
otel_headers: str | None = None # OTLP 鉴权头,形如 "k=v,k=v";当前 endpoint 裸跑,留空
otel_export_interval_ms: int = 5000
# 解析失败时把页面 HTML 作为 span event 上报的上限字节;超出截断并标注。
# 单个搜索页 HTML 可达 200KB-2MB,调高时同步关注 OTLP 单次请求大小限制。
otel_snapshot_max_bytes: int = 2_000_000
# ---- 登录态与下单配置(仅交易服务)---- # ---- 登录态与下单配置(仅交易服务)----
# 人工登录一次后落盘的 Playwright storage_state 目录(相对项目根目录)。 # 人工登录一次后落盘的 Playwright storage_state 目录(相对项目根目录)。
# 目录里是可直接冒充账号的 cookie,务必不要提交到版本库。 # 目录里是可直接冒充账号的 cookie,务必不要提交到版本库。
+145
View File
@@ -0,0 +1,145 @@
"""OpenTelemetry traces 接入:仅在 otel_enabled=true 时初始化,否则全 noop
抓取与交易两个进程在 lifespan 启动时各自调用 `setup_telemetry(settings,
service_name=...)`:注册 TracerProvider + OTLP/HTTP exporter + 自动
instrumentation(FastAPI、httpx)。失败时(如 endpoint 不可达)不阻断主流程,
仅打日志;traces 是辅助观测,不应让进程起不来。
`shutdown_telemetry` 在 lifespan 关闭时 force_flush 后再 shutdown,确保缓冲区
里的 span 都已上报。
OTel SDK 默认的 ProxyTracerProvider 在 setup 之前就能用(noop span),所以
其它代码里直接 `trace.get_tracer(__name__)` + `start_as_current_span` 即可,
不必关心 telemetry 是否启用——禁用时 span 不会真正产生与上报。
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace.sampling import ALWAYS_ON
from opentelemetry.trace import Span
from opentelemetry.util.types import AttributeValue
from app.shared.config import Settings
if TYPE_CHECKING:
from fastapi import FastAPI
logger = logging.getLogger(__name__)
# 全局 provider 引用,用于 instrument_app / shutdown 时判断当前是否已初始化。
# 显式持有比依赖 trace.get_tracer_provider() 的类型判断更稳——后者在测试场景
# 下可能被其它用例改动全局状态。
_provider: TracerProvider | None = None
def setup_telemetry(settings: Settings, *, service_name: str) -> None:
"""初始化 OTel:TracerProvider + OTLP exporter + httpx 自动 instrumentation。
- `otel_enabled=False` 或 endpoint 未配时仅打日志,不做任何事。
- 必须在创建任何 httpx.AsyncClient 之前调用,否则 httpx 不会被打桩。
两侧 main.py 的 lifespan 已把 setup 放在 site_session.start() 之前。
- 重复调用安全(_provider 已设时直接返回)。
"""
global _provider
if _provider is not None:
return
if not settings.otel_enabled or not settings.otel_endpoint:
logger.info("OpenTelemetry 未启用(endpoint 或 otel_enabled 未配置)")
return
resource = Resource.create({SERVICE_NAME: service_name})
provider = TracerProvider(resource=resource, sampler=ALWAYS_ON)
exporter = OTLPSpanExporter(
endpoint=settings.otel_endpoint,
headers=_parse_headers(settings.otel_headers),
timeout=10,
)
provider.add_span_processor(
BatchSpanProcessor(
exporter,
schedule_delay_millis=settings.otel_export_interval_ms,
)
)
trace.set_tracer_provider(provider)
_provider = provider
# httpx 是抓取/交易两侧唯一的外部 HTTP 客户端,打桩后所有 AsyncClient 请求
# 自动产生 CLIENT span。失败不影响主链路:进程仍可运行,只是看不到 span。
try:
HTTPXClientInstrumentor().instrument()
except Exception:
logger.warning("httpx 自动 instrumentation 失败", exc_info=True)
logger.info(
"OpenTelemetry 已启用:endpoint=%s service=%s",
settings.otel_endpoint,
service_name,
)
def instrument_app(app: "FastAPI") -> None:
"""FastAPI 应用打桩;未初始化时 noop,调用顺序无要求。"""
if _provider is None:
return
FastAPIInstrumentor.instrument_app(app)
def shutdown_telemetry() -> None:
"""flush + shutdown;幂等,未初始化时直接返回。"""
global _provider
if _provider is None:
return
try:
_provider.force_flush()
_provider.shutdown()
except Exception:
logger.debug("关闭 OpenTelemetry provider 失败", exc_info=True)
_provider = None
def snapshot(span: Span, name: str, html: str | None, max_bytes: int) -> None:
"""把 HTML 作为 span event 上报,超 max_bytes 截断并标注。
用于解析失败时复现页面:span 自身只放结构化指标(items 数、source 等),
完整 HTML 体量大、含商品/价格内容,仅在失败分支通过 event 携带。
"""
if html is None or not html:
return
original_bytes = len(html)
truncated = original_bytes > max_bytes
payload = html if not truncated else html[:max_bytes]
attributes: dict[str, AttributeValue] = {
"snapshot.html": payload,
"snapshot.original_bytes": original_bytes,
}
if truncated:
attributes["snapshot.truncated"] = True
span.add_event(name, attributes=attributes)
def _parse_headers(raw: str | None) -> list[tuple[str, str]] | None:
"""解析 "k1=v1,k2=v2" 形式的 header 配置;空输入返回 None。"""
if not raw or not raw.strip():
return None
pairs: list[tuple[str, str]] = []
for chunk in raw.split(","):
key, sep, value = chunk.partition("=")
if not sep or not key.strip():
continue
pairs.append((key.strip(), value.strip()))
return pairs or None
def is_initialized() -> bool:
"""供测试断言使用。"""
return _provider is not None
+5
View File
@@ -22,6 +22,7 @@ from fastapi import FastAPI
from app.shared.api import register_exception_handlers from app.shared.api import register_exception_handlers
from app.shared.config import get_settings from app.shared.config import get_settings
from app.shared.logging_setup import configure_logging from app.shared.logging_setup import configure_logging
from app.shared.telemetry import instrument_app, setup_telemetry, shutdown_telemetry
from app.trading.api.routes.auth import router as auth_router from app.trading.api.routes.auth import router as auth_router
from app.trading.api.routes.health import router as health_router from app.trading.api.routes.health import router as health_router
from app.trading.container import TradingContainer from app.trading.container import TradingContainer
@@ -43,6 +44,8 @@ async def lifespan(app: FastAPI):
app.state.container = container app.state.container = container
configure_logging(container.settings) configure_logging(container.settings)
# 与抓取侧同样:必须在创建 httpx 客户端之前 setup。
setup_telemetry(container.settings, service_name="rakuten-trading")
logger.info( logger.info(
"交易服务启动:%s:%s", "交易服务启动:%s:%s",
container.settings.trading_host, container.settings.trading_host,
@@ -54,6 +57,7 @@ async def lifespan(app: FastAPI):
yield yield
finally: finally:
await container.auth_session.close() await container.auth_session.close()
shutdown_telemetry()
def create_app() -> FastAPI: def create_app() -> FastAPI:
@@ -62,6 +66,7 @@ def create_app() -> FastAPI:
app.include_router(health_router) app.include_router(health_router)
app.include_router(auth_router) app.include_router(auth_router)
register_exception_handlers(app) register_exception_handlers(app)
instrument_app(app)
return app return app
+5
View File
@@ -7,6 +7,11 @@ dependencies = [
"fastapi>=0.115.0,<1.0.0", "fastapi>=0.115.0,<1.0.0",
"httpx[http2]>=0.27.0,<1.0.0", "httpx[http2]>=0.27.0,<1.0.0",
"loguru>=0.7.0,<1.0.0", "loguru>=0.7.0,<1.0.0",
"opentelemetry-api>=1.27.0,<2.0.0",
"opentelemetry-exporter-otlp>=1.27.0,<2.0.0",
"opentelemetry-instrumentation-fastapi>=0.48b0,<1.0.0",
"opentelemetry-instrumentation-httpx>=0.48b0,<1.0.0",
"opentelemetry-sdk>=1.27.0,<2.0.0",
"pydantic>=2.0.0,<3.0.0", "pydantic>=2.0.0,<3.0.0",
"pydantic-settings>=2.4.0,<3.0.0", "pydantic-settings>=2.4.0,<3.0.0",
"selectolax>=0.3.21,<1.0.0", "selectolax>=0.3.21,<1.0.0",
+48
View File
@@ -0,0 +1,48 @@
"""telemetry 模块测试
验证两件事:
1. 默认配置(otel_enabled=False)下 setup 是 noop,不初始化任何 provider。
2. enabled=true + endpoint 时 setup 注册真实 TracerProvider;shutdown 复位。
不打真实网络:OTLPSpanExporter 创建时不发请求,BatchSpanProcessor 异步批量
上报在没有 span 产生时也不会触发。
"""
from __future__ import annotations
from opentelemetry import trace
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from app.shared.config import Settings
from app.shared.telemetry import is_initialized, setup_telemetry, shutdown_telemetry
def test_disabled_is_noop():
"""otel_enabled=False 时 setup/instrument/shutdown 都不初始化 provider"""
settings = Settings(_env_file=None) # 默认 otel_enabled=False
setup_telemetry(settings, service_name="test-disabled")
assert not is_initialized()
def test_enabled_initializes_and_shutdown_releases():
"""enabled=true 时 setup 注册 TracerProvider,shutdown 后 _provider 复位"""
settings = Settings(
_env_file=None,
otel_enabled=True,
otel_endpoint="http://localhost:4318/v1/traces",
)
try:
setup_telemetry(settings, service_name="test-enabled")
assert is_initialized()
provider = trace.get_tracer_provider()
# 初始化后是真实 SDK provider(class 名是 TracerProvider);
# SDK 启动前的占位 provider 不属于这个类型
assert isinstance(provider, TracerProvider)
finally:
shutdown_telemetry()
# 解除 httpx 全局 monkey-patch,避免污染其它测试
try:
HTTPXClientInstrumentor().uninstrument()
except Exception:
pass
assert not is_initialized()
Generated
+390
View File
@@ -1,6 +1,10 @@
version = 1 version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = ">=3.13"
resolution-markers = [
"python_full_version >= '3.14'",
"python_full_version < '3.14'",
]
[[package]] [[package]]
name = "annotated-doc" name = "annotated-doc"
@@ -32,6 +36,15 @@ wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" }, { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" },
] ]
[[package]]
name = "asgiref"
version = "3.12.1"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.7.22" version = "2026.7.22"
@@ -41,6 +54,54 @@ wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775" }, { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775" },
] ]
[[package]]
name = "charset-normalizer"
version = "3.4.9"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5" },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.4.2" version = "8.4.2"
@@ -78,6 +139,18 @@ wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/eb/76/6d9e25ad88da9d3ff744bcdbec4736e38c2288611d43f673a5d9bfa27c07/fastapi-0.140.0-py3-none-any.whl", hash = "sha256:e951c0a0d9540bf5d9a2a9e078fd415da2ab7e312d435139e7d9e2e7fe9f0b23" }, { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/eb/76/6d9e25ad88da9d3ff744bcdbec4736e38c2288611d43f673a5d9bfa27c07/fastapi-0.140.0-py3-none-any.whl", hash = "sha256:e951c0a0d9540bf5d9a2a9e078fd415da2ab7e312d435139e7d9e2e7fe9f0b23" },
] ]
[[package]]
name = "googleapis-common-protos"
version = "1.75.0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed" },
]
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.5.4" version = "3.5.4"
@@ -135,6 +208,37 @@ wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/93/e8/65e8707d00fe2a49bf12f609a9b2b39ba6dd23c2810eacad877c4fc94bfe/greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994" }, { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/93/e8/65e8707d00fe2a49bf12f609a9b2b39ba6dd23c2810eacad877c4fc94bfe/greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994" },
] ]
[[package]]
name = "grpcio"
version = "1.83.0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/0c/98/304898ac4e04e2d5e4e4c2eadc178b1f2a16d5f4bc2f91306c87d64680b9/grpcio-1.83.0.tar.gz", hash = "sha256:7674587248fbbb2ac6e4eecf83a8a0f3d91a928f941de571acfd3a2f007fbc24" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/56/eb/135daaa713f32d33b8f99b4153b3f8dc3b2a124996ac15581bf9ebdad3c3/grpcio-1.83.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:6662f3b1e07cc7493d437351860dc867bddc6a93c83ecf33bbfdaf0c217ab2d0" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/8f/a1/121806ce69f23138dabe06aa595b0e5f1ae051a37e4c1954eed7d692c800/grpcio-1.83.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:74fe6f9e8a35c7dbf32255ee154d15e3e5338a81ed39173d079d594d2e544cd1" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/b0/e8/d0389e09cd6b4c4d3089b92967ae4e3ffd64795bd349bf2f85cd6656d3da/grpcio-1.83.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10b3fa0475eb572c9a81a6fe37fa16a9c500c0c91cfc148cac15692b7e3c2867" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/f8/51/f464c1d211fa50d5adbabe1b2e519948d99c13757052bfc9ea7afa28e284/grpcio-1.83.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5f20a988480b0f28207f057f7f7ae1313393c3cef0adcfeae8248f9947eaf881" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/e8/c0/539fe0832f2dd6500a28f5263071623fb34e8d4867aec632ccf81bd21156/grpcio-1.83.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bd82671b39065ba18cd536e9cd45b27ff649053f81ddd2c6a966d595067080f" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/8c/ca/ccf617d37ffa72567fa8e005ec7090c99da922799be2fb9847c8b21ca18c/grpcio-1.83.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc60215b5cb9fc8ca72942c498b551ac2305bd08f6ef8d4e3f0d21b64fbecd61" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/eb/b9/fd8d5245f823a8e0fd35d90e20ea3aa4acd47f8d5318fa8df307df52dec6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f1c3e5689d4b90987b1d72022bcfe866a9a3dc66197484cf856d96b6150e7f45" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/14/1e/f37632fc11db72dfa4bba86c3a43e54358e53030df111ecae5e91a733ad6/grpcio-1.83.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a21cb4eeeba124443f399be2e8b624943cde864dcbe588cb42e5c483a52a906c" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/93/b6/d70b69ae5c0cfc341b9ba474980e4ed99cbf05c0e4a14e9eee8cb73db0a5/grpcio-1.83.0-cp313-cp313-win32.whl", hash = "sha256:8fe04f1050a59f875601eb55d42b4f66946fe89817f967e34db1462ccd07dadf" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/0f/13/45d4cccb555cf4c476226979bf3d2fd0b0254216f7564c3a053e35117efc/grpcio-1.83.0-cp313-cp313-win_amd64.whl", hash = "sha256:6e01ecd9d8ef280abe1365138a4dc318f9a5287f4cb1b41d07816f796653f735" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/9c/60/f2cca8147ea213d3e43ae9158d03ad04e020fdf32ff027253e1fe93f921d/grpcio-1.83.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3f351629f6ae16ecc0ec3553e586a6763ffd9f6114044286d0cbec3e09241bfa" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/d0/ab/d3874931d123a95e83a3ebf8aa04537988fb62425cedb8bf3cefc5ad41b2/grpcio-1.83.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d05ff664100d429335b93c91b8b34ddf9e94a112205e7fa06dede309e44a4e4c" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/92/ff/6f18f9426b69306f4e00a9add3b0ee2748da8aad53836ef80cab0d62d04f/grpcio-1.83.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7936f2a56cf04f6514705c0fedf400971de01b6aa1719327e4718f410a765e2b" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/70/21/706d1147c6b93b98f179240c13991fbcc56880eba0c868abb1ad40d8a0a6/grpcio-1.83.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b0a0be840e51b6b7ee9df9269770faf77bdf4b771053c257c21d12bad607714c" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/74/04/1a8443c889115ec9e213a213e86bc93a71ee9088027e5befa09aaa0edd9d/grpcio-1.83.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:009667eaf3dcd5224c713589cdc98e7ca4ed0ff0b61132c6b276e930eb83a2df" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/86/c6/94e0fee5b12bc1da1370185b680988db6f739d19b42d9959db01a7ea50bf/grpcio-1.83.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb669918fd88936b15599caff4160a77ab74bdeb25f2231f6e45b61282d6107b" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/a0/97/de1ccb671fb85575bc5192faedf9ecdbdf5b390d2e6584dcf552bcbd370e/grpcio-1.83.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c19b454d3d3f28db81f2c7c4dbaee96e7f6fd149721733ffe79d6bc530f17404" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/17/0f/0e0ec749a7034ffcbaa050e39779872950ead90c22e7e0116be3f28b2b46/grpcio-1.83.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:61007cd08640abc5c54547ee32505474c482cd733a53cb87551ea81faa6350af" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/83/fa/c3fda157287f64bc65acee6c5aa90c41acf9e0d3a8e69a265eecff6d00a1/grpcio-1.83.0-cp314-cp314-win32.whl", hash = "sha256:32e11c37f5285b0c6fa3042c05fe06903696689749833fc64e67dec71b9bbe33" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/a1/00/b1b26431c9d54eee11724fd6e5585473a2ed47fbc1fb95e5204906a642ce/grpcio-1.83.0-cp314-cp314-win_amd64.whl", hash = "sha256:2bb48cb5e6dd005ca12b89ce4b6ac0b48ff3112c747542ee7986ef611a8ca6d9" },
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.16.0"
@@ -268,6 +372,190 @@ wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c" }, { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c" },
] ]
[[package]]
name = "opentelemetry-api"
version = "1.44.0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef" },
]
[[package]]
name = "opentelemetry-exporter-otlp"
version = "1.44.0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/f2/45/7af37fe54e5d3e66e7dcd7ba8b8aeee73f202bfac909cc94b8c4e428f9ac/opentelemetry_exporter_otlp-1.44.0.tar.gz", hash = "sha256:af1cde7c33ea8ed624bf04ac49a885730fe44c1f1ad698656e592c38f70ce106" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/18/c3/7b466a9463944e70b37b744072a0c1b88a425dade3fff0631adec66c9bcc/opentelemetry_exporter_otlp-1.44.0-py3-none-any.whl", hash = "sha256:4a498fa8d8fd8be9e8e2d175fe5524a3fe581ccffadd8509db86526a5fb97051" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.44.0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-grpc"
version = "1.44.0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "grpcio" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/1f/47/80d9e9d468dc5de3af5096f5ccdb065fa4dd1470f74495cc53e59e397f47/opentelemetry_exporter_otlp_proto_grpc-1.44.0.tar.gz", hash = "sha256:40d1ae9e03fcc36de3cbac610cc99f35894938bff9cfd90fc4ec68bd85448463" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/54/29/6ae42ba32b153ae0a44ae125f0caff2188bbe62d99c82d1768da30864e72/opentelemetry_exporter_otlp_proto_grpc-1.44.0-py3-none-any.whl", hash = "sha256:6a1a645ea182a2f59440c51fa8301d309f3324a8f9d65f8395584b064b67ee4e" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.44.0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/1a/87/95e2a5aaa795b4e2260d74e16df2d5541deb2ea9de010bcd615f4dee2654/opentelemetry_exporter_otlp_proto_http-1.44.0.tar.gz", hash = "sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/cd/d0/fdeb1a98d8d3a6205f5f297c51b4a9bfe65126ab60339669bbe3dd54c2e2/opentelemetry_exporter_otlp_proto_http-1.44.0-py3-none-any.whl", hash = "sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3" },
]
[[package]]
name = "opentelemetry-instrumentation"
version = "0.65b0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "packaging" },
{ name = "wrapt" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/40/7b/85eab1215f72adf0e68d3dc4a679b9bff993fa679ff34cd8dd378e2659fd/opentelemetry_instrumentation-0.65b0-py3-none-any.whl", hash = "sha256:ea967a72b9939b5fcfdad572753b4306c59dcb99e3f382d95dae04286805e137" },
]
[[package]]
name = "opentelemetry-instrumentation-asgi"
version = "0.65b0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/17/83/8e8e83b7ac285281687c7be2fd305213ccccbb8c0a2dd4fb45a8ccaf12c7/opentelemetry_instrumentation_asgi-0.65b0.tar.gz", hash = "sha256:892bca67c56522ffa85a8a83cf934d7b50b3be2132e45cbee705825f0a5ba426" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/0b/9c/376962840b619d2d55fe8ee2285f8c70971c090e5fff614516fc654a6f3a/opentelemetry_instrumentation_asgi-0.65b0-py3-none-any.whl", hash = "sha256:3a845a8ebd1c4ef0d8263401e6545f5b219b2feee612090d50f578a87e71fd65" },
]
[[package]]
name = "opentelemetry-instrumentation-fastapi"
version = "0.65b0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-instrumentation-asgi" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/30/23/b057f8196d06efdc1b50e3ff11fbc499a7d96b35c87f217eb7885542f4ea/opentelemetry_instrumentation_fastapi-0.65b0.tar.gz", hash = "sha256:10a3a95486036230413a58fe4fdf4a83fa6bba46918407e527476994bd92bd97" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/fa/b0/c9b0300d33349ecc3dfd2362516eaffc44877e90970e6a52178ff953fec3/opentelemetry_instrumentation_fastapi-0.65b0-py3-none-any.whl", hash = "sha256:cda2610a0ec1b22d19886f33e4d861e9f5dbb886aeaa3a1263b47aff82c36943" },
]
[[package]]
name = "opentelemetry-instrumentation-httpx"
version = "0.65b0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
{ name = "wrapt" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/61/03/a529140241addd4d0acc73bafbd6f74691651b92fc0ae9b4513cf80f07fa/opentelemetry_instrumentation_httpx-0.65b0.tar.gz", hash = "sha256:4627aa9c6bb99bf4462c8b565b0ef6aeb9ffad95c6c92868be1ef7895de112ee" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/9d/0f/c6144096b4914bbf44b43ba21c962e8f333ff045770b50a3e79ed8bd455f/opentelemetry_instrumentation_httpx-0.65b0-py3-none-any.whl", hash = "sha256:400f1b78afa4ee2332b5debe58e1ed1b317913d58812c952576be76660aeadb1" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.44.0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.44.0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.65b0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb" },
]
[[package]]
name = "opentelemetry-util-http"
version = "0.65b0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/32/a9/d7525a59fdd240e69b5af4a6338e78fafa1b4203394122cbd6701fb5f84a/opentelemetry_util_http-0.65b0.tar.gz", hash = "sha256:84f82d826978bba416ab453460ff6a7391cdc3534c93a786595e4068680016b7" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/23/3f/ab8d29df207ce5f470a07fa96ebb48af4e95b7fab7e7635311b9a32f2fab/opentelemetry_util_http-0.65b0-py3-none-any.whl", hash = "sha256:7553b606f963097cb190536dc30556cce85090692e471a422fff30ca29b04348" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "26.2" version = "26.2"
@@ -305,6 +593,21 @@ wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" }, { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" },
] ]
[[package]]
name = "protobuf"
version = "7.35.1"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9" },
]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.13.4" version = "2.13.4"
@@ -492,6 +795,11 @@ dependencies = [
{ name = "fastapi" }, { name = "fastapi" },
{ name = "httpx", extra = ["http2"] }, { name = "httpx", extra = ["http2"] },
{ name = "loguru" }, { name = "loguru" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-instrumentation-httpx" },
{ name = "opentelemetry-sdk" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "selectolax" }, { name = "selectolax" },
@@ -512,6 +820,11 @@ requires-dist = [
{ name = "fastapi", specifier = ">=0.115.0,<1.0.0" }, { name = "fastapi", specifier = ">=0.115.0,<1.0.0" },
{ name = "httpx", extras = ["http2"], specifier = ">=0.27.0,<1.0.0" }, { name = "httpx", extras = ["http2"], specifier = ">=0.27.0,<1.0.0" },
{ name = "loguru", specifier = ">=0.7.0,<1.0.0" }, { name = "loguru", specifier = ">=0.7.0,<1.0.0" },
{ name = "opentelemetry-api", specifier = ">=1.27.0,<2.0.0" },
{ name = "opentelemetry-exporter-otlp", specifier = ">=1.27.0,<2.0.0" },
{ name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.48b0,<1.0.0" },
{ name = "opentelemetry-instrumentation-httpx", specifier = ">=0.48b0,<1.0.0" },
{ name = "opentelemetry-sdk", specifier = ">=1.27.0,<2.0.0" },
{ name = "playwright", marker = "extra == 'browser'", specifier = ">=1.47.0,<2.0.0" }, { name = "playwright", marker = "extra == 'browser'", specifier = ">=1.47.0,<2.0.0" },
{ name = "pydantic", specifier = ">=2.0.0,<3.0.0" }, { name = "pydantic", specifier = ">=2.0.0,<3.0.0" },
{ name = "pydantic-settings", specifier = ">=2.4.0,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.4.0,<3.0.0" },
@@ -522,6 +835,21 @@ requires-dist = [
] ]
provides-extras = ["browser", "dev"] provides-extras = ["browser", "dev"]
[[package]]
name = "requests"
version = "2.34.2"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" },
]
[[package]] [[package]]
name = "selectolax" name = "selectolax"
version = "0.4.11" version = "0.4.11"
@@ -590,6 +918,15 @@ wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7" }, { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7" },
] ]
[[package]]
name = "urllib3"
version = "2.7.0"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" },
]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.51.0" version = "0.51.0"
@@ -779,3 +1116,56 @@ sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/b3/8f/
wheels = [ wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390" }, { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390" },
] ]
[[package]]
name = "wrapt"
version = "2.2.2"
source = { registry = "https://mirrors.huaweicloud.com/repository/pypi/simple" }
sdist = { url = "https://mirrors.huaweicloud.com/repository/pypi/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302" }
wheels = [
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/43/fc/f32f4b22c6511173c11d9e541ab4e7d8467a0f1b3455acaf784115d31ff8/wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/72/06/4d117d5d77a9344776c0248b24dae3d3dd2f58e5f765fa08cf887072e719/wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/15/ff/63ad96f98eb58a742b1a20d80f21da88924405910149950b912368150468/wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/20/1f/8bb62d8933df7acf3247194e6e9fc68edf9d2fa203252c89c94b319dd472/wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/17/09/8789dcb09ee1de715727db7521aabbb68ffa68dfade3a49468440cfced49/wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/9c/20/66e02562d53ee67d841f175e38e3c993c2d78a3e104c576cad61c028b43c/wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/bd/a3/832ac4e41222fb263b3042d42c2f08d305db7d0f0c9b1d3a271a9eede8f6/wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/b7/01/1bd5e4d2df9c0178989ac8da9186543465388588ee2ef153e2591accebef/wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/1c/69/583ed25291ab53e1ec117135fb1c33425e2f46d2bc8f29c17f7a94cf4274/wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/29/68/e69fc6d06e1523c68e0d00f95c9aed1158ce9908ee41603f7f2eae3d5db6/wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/55/21/fe7a393d9e5dc0923bed8f5d857e9dcff210f1fa0888c02cc8f3ffaa55aa/wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/b6/e5/c120d13bf5091164f68c3c1657e84f16f57e71d978421b626393ac5bd7eb/wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/d3/b0/d4a1eb97e0e286625bdf21bc7f702637f9607787ffbbdb5ec14d50c79dbf/wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/18/1e/f060df47755e87b57684cee7bfc1362b204df55fac96ffebc0631b697b79/wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/c4/de/2316a757a1abb6453700b79d83e532146dcef2611348282d4d8889792161/wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/ed/29/d1160785ae18ca2495a6d82a21154103d74f656c9fd457fb35f6b11b965a/wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/f5/2d/7caa9598ae61a9cf0989cc501739cbeeb7d650ab3193cca1407b9af0c6ab/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/ac/02/281ea1088b8650d865f311b35cf86fd21df89128e2909714f1161e01c9d0/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/be/7d/976e2d5b4b5c5babda40974edd54d0a5585cb60132ed86b46f4b80239b16/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/59/b7/e47651797c097f75a37e2ce86dcf04048ff576f3a674f7c558df7b5e9622/wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/d1/6f/9fa5d59fb06d890defb5a8f727ce6a14d2932c8760153f96956628559fee/wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/15/80/4c7bd9873d1f9f7d138d93556b500469dbe24f42710b877519c2b9eb380d/wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347" },
{ url = "https://mirrors.huaweicloud.com/repository/pypi/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048" },
]