feat: 执行并提交 PDD 采集任务 (#32)
This commit is contained in:
@@ -3,20 +3,20 @@
|
||||
import json
|
||||
import socket
|
||||
from http.client import RemoteDisconnected
|
||||
from typing import Callable, Mapping, Optional
|
||||
from typing import Any, Callable, Mapping, Optional
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import ProxyHandler, Request, build_opener
|
||||
from uuid import uuid4
|
||||
|
||||
from .admin_gateway import (
|
||||
AdminGateway,
|
||||
AdminTask,
|
||||
AdminGatewayError,
|
||||
ClaimCapabilities,
|
||||
ClientInfo,
|
||||
ClientRegistrationGateway,
|
||||
RegistrationReceipt,
|
||||
TaskClaimGateway,
|
||||
SubmissionReceipt,
|
||||
)
|
||||
from .task_models import TaskType
|
||||
|
||||
@@ -24,8 +24,8 @@ from .task_models import TaskType
|
||||
DEFAULT_ADMIN_BASE_URL = "http://127.0.0.1:8080"
|
||||
|
||||
|
||||
class HttpAdminGateway(ClientRegistrationGateway, TaskClaimGateway):
|
||||
"""通过 HTTP 登记 Client 和领取采集任务;令牌只保存在内存。"""
|
||||
class HttpAdminGateway(AdminGateway):
|
||||
"""通过 HTTP 登记、领取和提交任务;令牌只保存在内存。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -33,6 +33,7 @@ class HttpAdminGateway(ClientRegistrationGateway, TaskClaimGateway):
|
||||
token: str = "",
|
||||
timeout_seconds: float = 3.0,
|
||||
opener: Optional[Callable] = None,
|
||||
client_id: str = "",
|
||||
):
|
||||
normalized_url = base_url.strip().rstrip("/")
|
||||
parsed = urlparse(normalized_url)
|
||||
@@ -44,6 +45,7 @@ class HttpAdminGateway(ClientRegistrationGateway, TaskClaimGateway):
|
||||
self._base_url = normalized_url
|
||||
self._token = token.strip()
|
||||
self._timeout_seconds = timeout_seconds
|
||||
self._client_id = client_id.strip()
|
||||
# Admin 通常运行在本机或局域网。明确禁用环境代理,避免
|
||||
# HTTP_PROXY 把 127.0.0.1 请求错误转发到代理服务器。
|
||||
self._opener = opener or build_opener(ProxyHandler({})).open
|
||||
@@ -54,6 +56,7 @@ class HttpAdminGateway(ClientRegistrationGateway, TaskClaimGateway):
|
||||
"""调用独立登记接口,不领取或修改任务。"""
|
||||
|
||||
request_id = str(uuid4())
|
||||
self._client_id = client.client_id.strip()
|
||||
payload = {
|
||||
"client": {"name": client.name.strip()},
|
||||
"supported_types": [
|
||||
@@ -146,6 +149,7 @@ class HttpAdminGateway(ClientRegistrationGateway, TaskClaimGateway):
|
||||
"""领取一个采集任务;Admin 返回 204 时返回 ``None``。"""
|
||||
|
||||
request_id = str(uuid4())
|
||||
self._client_id = client.client_id.strip()
|
||||
payload = {
|
||||
"client": {"name": client.name.strip()},
|
||||
# #30 只允许领取采集任务。采购能力必须由安全门禁工单开启。
|
||||
@@ -219,6 +223,104 @@ class HttpAdminGateway(ClientRegistrationGateway, TaskClaimGateway):
|
||||
)
|
||||
return self._parse_claim_response(body, request_id)
|
||||
|
||||
def submit_result(
|
||||
self,
|
||||
task_id: str,
|
||||
idempotency_key: str,
|
||||
result: Mapping[str, Any],
|
||||
) -> SubmissionReceipt:
|
||||
"""幂等提交成功结果。"""
|
||||
|
||||
return self._submit(task_id, idempotency_key, result, "result")
|
||||
|
||||
def submit_failure(
|
||||
self,
|
||||
task_id: str,
|
||||
idempotency_key: str,
|
||||
failure: Mapping[str, Any],
|
||||
) -> SubmissionReceipt:
|
||||
"""幂等提交失败或人工处理结果。"""
|
||||
|
||||
return self._submit(task_id, idempotency_key, failure, "failure")
|
||||
|
||||
def _submit(
|
||||
self,
|
||||
task_id: str,
|
||||
idempotency_key: str,
|
||||
payload: Mapping[str, Any],
|
||||
endpoint: str,
|
||||
) -> SubmissionReceipt:
|
||||
if not task_id.strip():
|
||||
raise ValueError("task_id 不能为空")
|
||||
if not idempotency_key.strip():
|
||||
raise ValueError("Idempotency-Key 不能为空")
|
||||
if not self._client_id:
|
||||
raise AdminGatewayError(
|
||||
"CLIENT_ID_MISSING",
|
||||
"提交结果前必须先配置 Client 设备号",
|
||||
False,
|
||||
)
|
||||
request_id = str(uuid4())
|
||||
headers = {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Accept": "application/json",
|
||||
"Idempotency-Key": idempotency_key.strip(),
|
||||
"X-Request-Id": request_id,
|
||||
"X-Client-Id": self._client_id,
|
||||
}
|
||||
if self._token:
|
||||
headers["Authorization"] = f"Bearer {self._token}"
|
||||
request = Request(
|
||||
f"{self._base_url}/api/v1/client/tasks/{task_id.strip()}/{endpoint}",
|
||||
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with self._opener(request, timeout=self._timeout_seconds) as response:
|
||||
status = getattr(response, "status", None)
|
||||
if status is None:
|
||||
status = response.getcode()
|
||||
body = response.read()
|
||||
except HTTPError as exc:
|
||||
self._raise_http_error(exc, request_id, "提交任务")
|
||||
except (
|
||||
URLError,
|
||||
RemoteDisconnected,
|
||||
ConnectionError,
|
||||
socket.timeout,
|
||||
TimeoutError,
|
||||
) as exc:
|
||||
reason = getattr(exc, "reason", exc)
|
||||
code = "ADMIN_TIMEOUT" if isinstance(reason, (socket.timeout, TimeoutError)) else "ADMIN_UNAVAILABLE"
|
||||
message = "Admin 提交请求超时,请稍后重试" if code == "ADMIN_TIMEOUT" else "无法连接 Admin,结果已保存在本地"
|
||||
raise AdminGatewayError(code, message, True, request_id) from exc
|
||||
if status not in (200, 201):
|
||||
raise AdminGatewayError(
|
||||
"ADMIN_UNEXPECTED_RESPONSE",
|
||||
f"Admin 返回了未预期的状态码 {status}",
|
||||
status >= 500,
|
||||
request_id,
|
||||
)
|
||||
data = self._decode_json(body, request_id)
|
||||
accepted = data.get("accepted")
|
||||
result_id = data.get("result_id")
|
||||
accepted_at = data.get("accepted_at")
|
||||
if (
|
||||
accepted is not True
|
||||
or not isinstance(result_id, str)
|
||||
or not result_id.strip()
|
||||
or not isinstance(accepted_at, str)
|
||||
or not accepted_at.strip()
|
||||
):
|
||||
raise AdminGatewayError(
|
||||
"ADMIN_INVALID_RESPONSE",
|
||||
"Admin 提交响应字段不完整",
|
||||
False,
|
||||
request_id,
|
||||
)
|
||||
return SubmissionReceipt(True, result_id, accepted_at)
|
||||
|
||||
@classmethod
|
||||
def _parse_claim_response(
|
||||
cls,
|
||||
|
||||
Reference in New Issue
Block a user