Files

349 lines
12 KiB
Python
Raw Permalink Normal View History

"""腾讯云人脸识别客户端(精简门面)。
原 1311 行的客户端已拆分为:
- ``retry.py``: 重试配置与执行器
- ``image_processor.py``: 图片获取/压缩/编码
- ``api_operations.py``: 各 API 操作的实现(搜索/检测/属性/人员/人脸/库信息/列表)
本文件仅保留:凭据创建、HTTP Session 管理、错误脱敏、以及对外暴露的
``TencentCloudClient`` 门面,委托给上述模块。
"""
from __future__ import annotations
2025-09-07 21:48:21 +08:00
import logging
from typing import Any, Dict, List, Optional
2025-09-07 21:48:21 +08:00
import requests
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import (
TencentCloudSDKException,
2025-09-07 21:48:21 +08:00
)
from tencentcloud.iai.v20200303 import iai_client
from . import api_operations
2025-09-07 21:48:21 +08:00
from .errors import (
ApiError,
AuthenticationError,
2025-09-07 21:48:21 +08:00
log_and_raise_error,
validate_config,
)
from .image_processor import ImageCache, get_image_base64
from .retry import RetryConfig
from .const import CONF_REGION, CONF_SECRET_ID, CONF_SECRET_KEY
2025-09-07 21:48:21 +08:00
_LOGGER = logging.getLogger(__name__)
class TencentCloudClient:
"""腾讯云人脸识别客户端门面。"""
2025-09-07 21:48:21 +08:00
def __init__(
self,
secret_id: str,
secret_key: str,
region: str = "ap-shanghai",
retry_config: Optional[RetryConfig] = None,
) -> None:
validate_config({
CONF_SECRET_ID: secret_id,
CONF_SECRET_KEY: secret_key,
CONF_REGION: region,
})
2025-09-07 21:48:21 +08:00
self.secret_id = secret_id
self.secret_key = secret_key
self.region = region
self.retry_config = retry_config or RetryConfig()
self.client = self._create_client()
# HTTP 连接池化
self._session = requests.Session()
self._session.mount(
"http://", requests.adapters.HTTPAdapter(max_retries=2)
)
self._session.mount(
"https://", requests.adapters.HTTPAdapter(max_retries=2)
)
# 图片缓存(url/path),通过 session 下载
self._image_cache = ImageCache(
maxsize=32,
download_fn=lambda url: get_image_base64(
image_url=url, session=self._session
),
read_fn=None,
)
# --- 资源管理 --------------------------------------------------------
def close(self) -> None:
"""释放 HTTP Session 等资源。"""
if self._session:
self._session.close()
self._image_cache.clear()
# --- 客户端创建 -------------------------------------------------------
2025-09-07 21:48:21 +08:00
def _create_client(self) -> iai_client.IaiClient:
try:
_LOGGER.debug("创建腾讯云客户端,区域: %s", self.region)
cred = credential.Credential(self.secret_id, self.secret_key)
client = iai_client.IaiClient(cred, self.region)
_LOGGER.debug("腾讯云客户端创建成功")
return client
except Exception as ex:
_LOGGER.error("创建腾讯云客户端失败: %s", self._sanitize_error(str(ex)))
2025-09-07 21:48:21 +08:00
log_and_raise_error(
AuthenticationError,
"创建腾讯云客户端失败",
{"region": self.region, "error_type": type(ex).__name__},
2025-09-07 21:48:21 +08:00
)
# --- 凭据验证 ---------------------------------------------------------
def verify_credentials(self) -> bool:
"""验证凭据(轻量 GetGroupList)。失败抛出对应错误。"""
return api_operations.verify_credentials(
self.client, retry_config=self.retry_config
)
def verify_credentials_lightweight(self) -> bool:
"""测试 API 连接(轻量验证)。失败返回 False,不抛异常。"""
try:
self.verify_credentials()
return True
except Exception as ex: # noqa: BLE001
_LOGGER.error("API连接测试失败: %s", self._sanitize_error(str(ex)))
return False
def test_api_connection(self, group_id: str) -> bool:
"""测试与指定人员库的连接。失败返回 False。"""
try:
_LOGGER.info("开始测试API连接: group_id=%s", group_id)
result = self.get_group_info(group_id)
if result.get("success", False):
_LOGGER.info("API连接测试成功")
return True
_LOGGER.error("API连接测试失败: %s", result.get("error_message"))
return False
except Exception as ex: # noqa: BLE001
_LOGGER.error("API连接测试失败: %s", self._sanitize_error(str(ex)))
return False
2025-09-07 21:48:21 +08:00
# 兼容旧调用名
_test_api_connection = test_api_connection
# --- 图片 ------------------------------------------------------------
def _get_image_base64(
self,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
) -> str:
"""获取图片 base64(带 url/path 缓存)。"""
if image_url:
return self._image_cache.get_url(image_url)
if image_path:
return self._image_cache.get_path(image_path)
if image_file:
return self._image_cache.get(image_file=image_file)
log_and_raise_error(
ApiError,
"必须提供 image_url、image_path 或 image_file",
{"reason": "no_image_source"},
2025-09-07 21:48:21 +08:00
)
# --- 业务 API(委托 api_operations ----------------------------------
2025-09-07 21:48:21 +08:00
def search_faces(
self,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
image_base64: Optional[str] = None,
group_ids: Optional[List[str]] = None,
2025-09-07 21:48:21 +08:00
max_face_num: int = 1,
min_face_size: int = 34,
max_user_num: int = 5,
quality_control: int = 1,
need_rotate_check: int = 1,
face_match_threshold: float = 60.0,
2025-09-07 23:21:24 +08:00
) -> Dict[str, Any]:
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
if not group_ids:
_LOGGER.warning("未提供人员库ID,搜索可能失败")
return api_operations.search_faces(
self.client,
image_base64=image_base64,
group_ids=group_ids or [],
max_face_num=max_face_num,
min_face_size=min_face_size,
max_user_num=max_user_num,
quality_control=quality_control,
need_rotate_check=need_rotate_check,
face_match_threshold=face_match_threshold,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
2025-09-07 21:48:21 +08:00
def detect_faces(
self,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
image_base64: Optional[str] = None,
2025-09-07 21:48:21 +08:00
max_face_num: int = 1,
min_face_size: int = 34,
need_rotate_check: int = 1,
2025-09-07 23:21:24 +08:00
) -> Dict[str, Any]:
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
return api_operations.detect_faces(
self.client,
image_base64=image_base64,
max_face_num=max_face_num,
min_face_size=min_face_size,
need_rotate_check=need_rotate_check,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
2025-09-07 21:48:21 +08:00
def get_face_attributes(
self,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
image_base64: Optional[str] = None,
2025-09-07 21:48:21 +08:00
max_face_num: int = 1,
need_rotate_check: int = 1,
2025-09-07 23:21:24 +08:00
) -> Dict[str, Any]:
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
return api_operations.get_face_attributes(
self.client,
image_base64=image_base64,
max_face_num=max_face_num,
need_rotate_check=need_rotate_check,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def create_person(
self,
person_id: str,
person_name: str,
group_id: str,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
image_base64: Optional[str] = None,
gender: Optional[int] = None,
person_tag: Optional[str] = None,
quality_control: int = 1,
need_rotate_check: int = 1,
) -> Dict[str, Any]:
if image_base64 is None and any([image_url, image_path, image_file]):
image_base64 = self._get_image_base64(image_url, image_path, image_file)
return api_operations.create_person(
self.client,
person_id=person_id,
person_name=person_name,
group_id=group_id,
image_base64=image_base64,
gender=gender,
person_tag=person_tag,
quality_control=quality_control,
need_rotate_check=need_rotate_check,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def delete_person(self, person_id: str) -> Dict[str, Any]:
return api_operations.delete_person(
self.client,
person_id=person_id,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def create_face(
self,
person_id: str,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
image_base64: Optional[str] = None,
quality_control: int = 1,
need_rotate_check: int = 1,
) -> Dict[str, Any]:
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
return api_operations.create_face(
self.client,
person_id=person_id,
image_base64=image_base64,
quality_control=quality_control,
need_rotate_check=need_rotate_check,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def delete_face(self, person_id: str, face_id: str) -> Dict[str, Any]:
return api_operations.delete_face(
self.client,
person_id=person_id,
face_id=face_id,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
2025-09-07 21:48:21 +08:00
def get_group_info(self, group_id: str) -> Dict[str, Any]:
return api_operations.get_group_info(
self.client,
group_id=group_id,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
2025-09-07 21:48:21 +08:00
def get_person_list(
self, group_id: str, limit: int = 100, offset: int = 0
) -> Dict[str, Any]:
return api_operations.get_person_list(
self.client,
group_id=group_id,
limit=limit,
offset=offset,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
2025-09-07 21:48:21 +08:00
def get_person_list_all(self, group_id: str) -> Dict[str, Any]:
return api_operations.get_person_list_all(
self.client,
group_id=group_id,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
2025-09-07 21:48:21 +08:00
)
# --- 辅助 ------------------------------------------------------------
def _sanitize_error(self, error_msg: str) -> str:
"""脱敏错误信息中的凭据。"""
if not isinstance(error_msg, str):
return str(error_msg)
if self.secret_id and self.secret_id in error_msg:
error_msg = error_msg.replace(
self.secret_id, f"{self.secret_id[:8]}***"
2025-09-07 21:48:21 +08:00
)
if self.secret_key and self.secret_key in error_msg:
error_msg = error_msg.replace(self.secret_key, "***")
return error_msg
# 兼容旧代码:暴露 TencentCloudSDKException 别名
@staticmethod
def is_sdk_exception(ex: Exception) -> bool:
return isinstance(ex, TencentCloudSDKException)