platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
623
selective-vpn-gui/api/transport_singbox.py
Normal file
623
selective-vpn-gui/api/transport_singbox.py
Normal file
@@ -0,0 +1,623 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
from .errors import ApiError
|
||||
from .models import *
|
||||
|
||||
|
||||
class TransportSingBoxApiMixin:
|
||||
def transport_singbox_profiles_get(
|
||||
self,
|
||||
*,
|
||||
enabled_only: bool = False,
|
||||
mode: str = "",
|
||||
protocol: str = "",
|
||||
) -> SingBoxProfilesState:
|
||||
params: Dict[str, Any] = {}
|
||||
if enabled_only:
|
||||
params["enabled_only"] = "true"
|
||||
mode_v = str(mode or "").strip().lower()
|
||||
if mode_v:
|
||||
params["mode"] = mode_v
|
||||
protocol_v = str(protocol or "").strip().lower()
|
||||
if protocol_v:
|
||||
params["protocol"] = protocol_v
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"GET",
|
||||
"/api/v1/transport/singbox/profiles",
|
||||
params=(params or None),
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return self._parse_singbox_profiles_state(data)
|
||||
|
||||
def transport_singbox_profile_get(self, profile_id: str) -> SingBoxProfile:
|
||||
pid = str(profile_id or "").strip()
|
||||
if not pid:
|
||||
raise ValueError("missing singbox profile id")
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(self._request("GET", f"/api/v1/transport/singbox/profiles/{pid}")) or {},
|
||||
)
|
||||
snap = self._parse_singbox_profiles_state(data)
|
||||
if snap.item is None:
|
||||
raise ApiError(
|
||||
"API returned malformed singbox profile payload",
|
||||
"GET",
|
||||
self._url(f"/api/v1/transport/singbox/profiles/{pid}"),
|
||||
)
|
||||
return snap.item
|
||||
|
||||
def transport_singbox_profile_create(
|
||||
self,
|
||||
*,
|
||||
profile_id: str = "",
|
||||
name: str = "",
|
||||
mode: str = "raw",
|
||||
protocol: str = "",
|
||||
enabled: Optional[bool] = None,
|
||||
schema_version: int = 1,
|
||||
typed: Optional[Dict[str, Any]] = None,
|
||||
raw_config: Optional[Dict[str, Any]] = None,
|
||||
meta: Optional[Dict[str, Any]] = None,
|
||||
secrets: Optional[Dict[str, str]] = None,
|
||||
) -> SingBoxProfilesState:
|
||||
payload: Dict[str, Any] = {
|
||||
"id": str(profile_id or "").strip(),
|
||||
"name": str(name or "").strip(),
|
||||
"mode": str(mode or "raw").strip().lower(),
|
||||
"protocol": str(protocol or "").strip().lower(),
|
||||
"schema_version": int(schema_version or 1),
|
||||
}
|
||||
if enabled is not None:
|
||||
payload["enabled"] = bool(enabled)
|
||||
if typed is not None:
|
||||
payload["typed"] = cast(Dict[str, Any], typed)
|
||||
if raw_config is not None:
|
||||
payload["raw_config"] = cast(Dict[str, Any], raw_config)
|
||||
if meta is not None:
|
||||
payload["meta"] = cast(Dict[str, Any], meta)
|
||||
if secrets is not None:
|
||||
payload["secrets"] = {
|
||||
str(k).strip(): str(v)
|
||||
for k, v in cast(Dict[str, Any], secrets).items()
|
||||
if str(k).strip()
|
||||
}
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"POST",
|
||||
"/api/v1/transport/singbox/profiles",
|
||||
json_body=payload,
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return self._parse_singbox_profiles_state(data)
|
||||
|
||||
def transport_singbox_profile_patch(
|
||||
self,
|
||||
profile_id: str,
|
||||
*,
|
||||
base_revision: int = 0,
|
||||
name: Optional[str] = None,
|
||||
mode: Optional[str] = None,
|
||||
protocol: Optional[str] = None,
|
||||
enabled: Optional[bool] = None,
|
||||
schema_version: Optional[int] = None,
|
||||
typed: Optional[Dict[str, Any]] = None,
|
||||
raw_config: Optional[Dict[str, Any]] = None,
|
||||
meta: Optional[Dict[str, Any]] = None,
|
||||
secrets: Optional[Dict[str, str]] = None,
|
||||
clear_secrets: bool = False,
|
||||
) -> SingBoxProfilesState:
|
||||
pid = str(profile_id or "").strip()
|
||||
if not pid:
|
||||
raise ValueError("missing singbox profile id")
|
||||
|
||||
payload: Dict[str, Any] = {}
|
||||
if int(base_revision or 0) > 0:
|
||||
payload["base_revision"] = int(base_revision)
|
||||
if name is not None:
|
||||
payload["name"] = str(name)
|
||||
if mode is not None:
|
||||
payload["mode"] = str(mode or "").strip().lower()
|
||||
if protocol is not None:
|
||||
payload["protocol"] = str(protocol or "").strip().lower()
|
||||
if enabled is not None:
|
||||
payload["enabled"] = bool(enabled)
|
||||
if schema_version is not None:
|
||||
payload["schema_version"] = int(schema_version)
|
||||
if typed is not None:
|
||||
payload["typed"] = cast(Dict[str, Any], typed)
|
||||
if raw_config is not None:
|
||||
payload["raw_config"] = cast(Dict[str, Any], raw_config)
|
||||
if meta is not None:
|
||||
payload["meta"] = cast(Dict[str, Any], meta)
|
||||
if secrets is not None:
|
||||
payload["secrets"] = {
|
||||
str(k).strip(): str(v)
|
||||
for k, v in cast(Dict[str, Any], secrets).items()
|
||||
if str(k).strip()
|
||||
}
|
||||
if clear_secrets:
|
||||
payload["clear_secrets"] = True
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"PATCH",
|
||||
f"/api/v1/transport/singbox/profiles/{pid}",
|
||||
json_body=payload,
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return self._parse_singbox_profiles_state(data)
|
||||
|
||||
def transport_singbox_profile_render(
|
||||
self,
|
||||
profile_id: str,
|
||||
*,
|
||||
base_revision: int = 0,
|
||||
check_binary: Optional[bool] = None,
|
||||
persist: Optional[bool] = None,
|
||||
) -> SingBoxProfileRenderResult:
|
||||
pid = str(profile_id or "").strip()
|
||||
if not pid:
|
||||
raise ValueError("missing singbox profile id")
|
||||
|
||||
payload: Dict[str, Any] = {}
|
||||
if int(base_revision or 0) > 0:
|
||||
payload["base_revision"] = int(base_revision)
|
||||
if check_binary is not None:
|
||||
payload["check_binary"] = bool(check_binary)
|
||||
if persist is not None:
|
||||
payload["persist"] = bool(persist)
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"POST",
|
||||
f"/api/v1/transport/singbox/profiles/{pid}/render",
|
||||
json_body=payload,
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return self._parse_singbox_profile_render(data, fallback_id=pid)
|
||||
|
||||
def transport_singbox_profile_validate(
|
||||
self,
|
||||
profile_id: str,
|
||||
*,
|
||||
base_revision: int = 0,
|
||||
check_binary: Optional[bool] = None,
|
||||
) -> SingBoxProfileValidateResult:
|
||||
pid = str(profile_id or "").strip()
|
||||
if not pid:
|
||||
raise ValueError("missing singbox profile id")
|
||||
|
||||
payload: Dict[str, Any] = {}
|
||||
if int(base_revision or 0) > 0:
|
||||
payload["base_revision"] = int(base_revision)
|
||||
if check_binary is not None:
|
||||
payload["check_binary"] = bool(check_binary)
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"POST",
|
||||
f"/api/v1/transport/singbox/profiles/{pid}/validate",
|
||||
json_body=payload,
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return self._parse_singbox_profile_validate(data, fallback_id=pid)
|
||||
|
||||
def transport_singbox_profile_apply(
|
||||
self,
|
||||
profile_id: str,
|
||||
*,
|
||||
client_id: str = "",
|
||||
config_path: str = "",
|
||||
restart: Optional[bool] = None,
|
||||
skip_runtime: bool = False,
|
||||
check_binary: Optional[bool] = None,
|
||||
base_revision: int = 0,
|
||||
) -> SingBoxProfileApplyResult:
|
||||
pid = str(profile_id or "").strip()
|
||||
if not pid:
|
||||
raise ValueError("missing singbox profile id")
|
||||
|
||||
payload: Dict[str, Any] = {}
|
||||
if int(base_revision or 0) > 0:
|
||||
payload["base_revision"] = int(base_revision)
|
||||
cid = str(client_id or "").strip()
|
||||
if cid:
|
||||
payload["client_id"] = cid
|
||||
path = str(config_path or "").strip()
|
||||
if path:
|
||||
payload["config_path"] = path
|
||||
if restart is not None:
|
||||
payload["restart"] = bool(restart)
|
||||
if bool(skip_runtime):
|
||||
payload["skip_runtime"] = True
|
||||
if check_binary is not None:
|
||||
payload["check_binary"] = bool(check_binary)
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"POST",
|
||||
f"/api/v1/transport/singbox/profiles/{pid}/apply",
|
||||
json_body=payload,
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return self._parse_singbox_profile_apply(data, fallback_id=pid, fallback_client=cid)
|
||||
|
||||
def transport_singbox_profile_rollback(
|
||||
self,
|
||||
profile_id: str,
|
||||
*,
|
||||
base_revision: int = 0,
|
||||
client_id: str = "",
|
||||
config_path: str = "",
|
||||
history_id: str = "",
|
||||
restart: Optional[bool] = None,
|
||||
skip_runtime: bool = False,
|
||||
) -> SingBoxProfileRollbackResult:
|
||||
pid = str(profile_id or "").strip()
|
||||
if not pid:
|
||||
raise ValueError("missing singbox profile id")
|
||||
|
||||
payload: Dict[str, Any] = {}
|
||||
if int(base_revision or 0) > 0:
|
||||
payload["base_revision"] = int(base_revision)
|
||||
cid = str(client_id or "").strip()
|
||||
if cid:
|
||||
payload["client_id"] = cid
|
||||
path = str(config_path or "").strip()
|
||||
if path:
|
||||
payload["config_path"] = path
|
||||
hid = str(history_id or "").strip()
|
||||
if hid:
|
||||
payload["history_id"] = hid
|
||||
if restart is not None:
|
||||
payload["restart"] = bool(restart)
|
||||
if bool(skip_runtime):
|
||||
payload["skip_runtime"] = True
|
||||
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"POST",
|
||||
f"/api/v1/transport/singbox/profiles/{pid}/rollback",
|
||||
json_body=payload,
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return self._parse_singbox_profile_rollback(data, fallback_id=pid, fallback_client=cid)
|
||||
|
||||
def transport_singbox_profile_history(
|
||||
self,
|
||||
profile_id: str,
|
||||
*,
|
||||
limit: int = 20,
|
||||
) -> SingBoxProfileHistoryResult:
|
||||
pid = str(profile_id or "").strip()
|
||||
if not pid:
|
||||
raise ValueError("missing singbox profile id")
|
||||
lim = int(limit or 0)
|
||||
if lim <= 0:
|
||||
lim = 20
|
||||
data = cast(
|
||||
Dict[str, Any],
|
||||
self._json(
|
||||
self._request(
|
||||
"GET",
|
||||
f"/api/v1/transport/singbox/profiles/{pid}/history",
|
||||
params={"limit": str(lim)},
|
||||
)
|
||||
)
|
||||
or {},
|
||||
)
|
||||
return self._parse_singbox_profile_history(data, fallback_id=pid)
|
||||
|
||||
# DNS / SmartDNS
|
||||
|
||||
def _parse_singbox_profile_issue(self, row: Any) -> Optional[SingBoxProfileIssue]:
|
||||
if not isinstance(row, dict):
|
||||
return None
|
||||
return SingBoxProfileIssue(
|
||||
field=str(row.get("field") or "").strip(),
|
||||
severity=str(row.get("severity") or "").strip().lower(),
|
||||
code=str(row.get("code") or "").strip(),
|
||||
message=str(row.get("message") or "").strip(),
|
||||
)
|
||||
|
||||
def _parse_singbox_profile_diff(self, raw: Any) -> SingBoxProfileRenderDiff:
|
||||
data = raw if isinstance(raw, dict) else {}
|
||||
return SingBoxProfileRenderDiff(
|
||||
added=self._to_int(data.get("added")),
|
||||
changed=self._to_int(data.get("changed")),
|
||||
removed=self._to_int(data.get("removed")),
|
||||
)
|
||||
|
||||
def _parse_singbox_profile(self, raw: Any) -> Optional[SingBoxProfile]:
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
pid = str(raw.get("id") or "").strip()
|
||||
if not pid:
|
||||
return None
|
||||
|
||||
typed_raw = raw.get("typed") or {}
|
||||
if not isinstance(typed_raw, dict):
|
||||
typed_raw = {}
|
||||
raw_cfg = raw.get("raw_config") or {}
|
||||
if not isinstance(raw_cfg, dict):
|
||||
raw_cfg = {}
|
||||
meta_raw = raw.get("meta") or {}
|
||||
if not isinstance(meta_raw, dict):
|
||||
meta_raw = {}
|
||||
masked_raw = raw.get("secrets_masked") or {}
|
||||
if not isinstance(masked_raw, dict):
|
||||
masked_raw = {}
|
||||
masked: Dict[str, str] = {}
|
||||
for k, v in masked_raw.items():
|
||||
key = str(k or "").strip()
|
||||
if not key:
|
||||
continue
|
||||
masked[key] = str(v or "")
|
||||
|
||||
return SingBoxProfile(
|
||||
id=pid,
|
||||
name=str(raw.get("name") or "").strip(),
|
||||
mode=str(raw.get("mode") or "").strip().lower(),
|
||||
protocol=str(raw.get("protocol") or "").strip().lower(),
|
||||
enabled=bool(raw.get("enabled", False)),
|
||||
schema_version=self._to_int(raw.get("schema_version"), default=1),
|
||||
profile_revision=self._to_int(raw.get("profile_revision")),
|
||||
render_revision=self._to_int(raw.get("render_revision")),
|
||||
last_validated_at=str(raw.get("last_validated_at") or "").strip(),
|
||||
last_applied_at=str(raw.get("last_applied_at") or "").strip(),
|
||||
last_error=str(raw.get("last_error") or "").strip(),
|
||||
typed=cast(Dict[str, Any], typed_raw),
|
||||
raw_config=cast(Dict[str, Any], raw_cfg),
|
||||
meta=cast(Dict[str, Any], meta_raw),
|
||||
has_secrets=bool(raw.get("has_secrets", False)),
|
||||
secrets_masked=masked,
|
||||
created_at=str(raw.get("created_at") or "").strip(),
|
||||
updated_at=str(raw.get("updated_at") or "").strip(),
|
||||
)
|
||||
|
||||
def _parse_singbox_profiles_state(self, data: Dict[str, Any]) -> SingBoxProfilesState:
|
||||
raw_items = data.get("items") or []
|
||||
if not isinstance(raw_items, list):
|
||||
raw_items = []
|
||||
items: List[SingBoxProfile] = []
|
||||
for row in raw_items:
|
||||
p = self._parse_singbox_profile(row)
|
||||
if p is not None:
|
||||
items.append(p)
|
||||
|
||||
item = self._parse_singbox_profile(data.get("item"))
|
||||
|
||||
return SingBoxProfilesState(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=str(data.get("message") or "").strip(),
|
||||
code=str(data.get("code") or "").strip(),
|
||||
count=self._to_int(data.get("count"), default=len(items)),
|
||||
active_profile_id=str(data.get("active_profile_id") or "").strip(),
|
||||
items=items,
|
||||
item=item,
|
||||
)
|
||||
|
||||
def _parse_singbox_profile_validate(
|
||||
self,
|
||||
data: Dict[str, Any],
|
||||
*,
|
||||
fallback_id: str = "",
|
||||
) -> SingBoxProfileValidateResult:
|
||||
raw_errors = data.get("errors") or []
|
||||
if not isinstance(raw_errors, list):
|
||||
raw_errors = []
|
||||
raw_warnings = data.get("warnings") or []
|
||||
if not isinstance(raw_warnings, list):
|
||||
raw_warnings = []
|
||||
|
||||
errors: List[SingBoxProfileIssue] = []
|
||||
for row in raw_errors:
|
||||
issue = self._parse_singbox_profile_issue(row)
|
||||
if issue is not None:
|
||||
errors.append(issue)
|
||||
|
||||
warnings: List[SingBoxProfileIssue] = []
|
||||
for row in raw_warnings:
|
||||
issue = self._parse_singbox_profile_issue(row)
|
||||
if issue is not None:
|
||||
warnings.append(issue)
|
||||
|
||||
return SingBoxProfileValidateResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=str(data.get("message") or "").strip(),
|
||||
code=str(data.get("code") or "").strip(),
|
||||
profile_id=str(data.get("profile_id") or fallback_id).strip(),
|
||||
profile_revision=self._to_int(data.get("profile_revision")),
|
||||
valid=bool(data.get("valid", False)),
|
||||
errors=errors,
|
||||
warnings=warnings,
|
||||
render_digest=str(data.get("render_digest") or "").strip(),
|
||||
diff=self._parse_singbox_profile_diff(data.get("diff")),
|
||||
)
|
||||
|
||||
def _parse_singbox_profile_apply(
|
||||
self,
|
||||
data: Dict[str, Any],
|
||||
*,
|
||||
fallback_id: str = "",
|
||||
fallback_client: str = "",
|
||||
) -> SingBoxProfileApplyResult:
|
||||
raw_errors = data.get("errors") or []
|
||||
if not isinstance(raw_errors, list):
|
||||
raw_errors = []
|
||||
raw_warnings = data.get("warnings") or []
|
||||
if not isinstance(raw_warnings, list):
|
||||
raw_warnings = []
|
||||
|
||||
errors: List[SingBoxProfileIssue] = []
|
||||
for row in raw_errors:
|
||||
issue = self._parse_singbox_profile_issue(row)
|
||||
if issue is not None:
|
||||
errors.append(issue)
|
||||
|
||||
warnings: List[SingBoxProfileIssue] = []
|
||||
for row in raw_warnings:
|
||||
issue = self._parse_singbox_profile_issue(row)
|
||||
if issue is not None:
|
||||
warnings.append(issue)
|
||||
|
||||
return SingBoxProfileApplyResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=str(data.get("message") or "").strip(),
|
||||
code=str(data.get("code") or "").strip(),
|
||||
profile_id=str(data.get("profile_id") or fallback_id).strip(),
|
||||
client_id=str(data.get("client_id") or fallback_client).strip(),
|
||||
config_path=str(data.get("config_path") or "").strip(),
|
||||
profile_revision=self._to_int(data.get("profile_revision")),
|
||||
render_revision=self._to_int(data.get("render_revision")),
|
||||
last_applied_at=str(data.get("last_applied_at") or "").strip(),
|
||||
render_path=str(data.get("render_path") or "").strip(),
|
||||
render_digest=str(data.get("render_digest") or "").strip(),
|
||||
rollback_available=bool(data.get("rollback_available", False)),
|
||||
valid=bool(data.get("valid", False)),
|
||||
errors=errors,
|
||||
warnings=warnings,
|
||||
diff=self._parse_singbox_profile_diff(data.get("diff")),
|
||||
)
|
||||
|
||||
def _parse_singbox_profile_render(
|
||||
self,
|
||||
data: Dict[str, Any],
|
||||
*,
|
||||
fallback_id: str = "",
|
||||
) -> SingBoxProfileRenderResult:
|
||||
raw_errors = data.get("errors") or []
|
||||
if not isinstance(raw_errors, list):
|
||||
raw_errors = []
|
||||
raw_warnings = data.get("warnings") or []
|
||||
if not isinstance(raw_warnings, list):
|
||||
raw_warnings = []
|
||||
raw_config = data.get("config") or {}
|
||||
if not isinstance(raw_config, dict):
|
||||
raw_config = {}
|
||||
|
||||
errors: List[SingBoxProfileIssue] = []
|
||||
for row in raw_errors:
|
||||
issue = self._parse_singbox_profile_issue(row)
|
||||
if issue is not None:
|
||||
errors.append(issue)
|
||||
|
||||
warnings: List[SingBoxProfileIssue] = []
|
||||
for row in raw_warnings:
|
||||
issue = self._parse_singbox_profile_issue(row)
|
||||
if issue is not None:
|
||||
warnings.append(issue)
|
||||
|
||||
return SingBoxProfileRenderResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=str(data.get("message") or "").strip(),
|
||||
code=str(data.get("code") or "").strip(),
|
||||
profile_id=str(data.get("profile_id") or fallback_id).strip(),
|
||||
profile_revision=self._to_int(data.get("profile_revision")),
|
||||
render_revision=self._to_int(data.get("render_revision")),
|
||||
render_path=str(data.get("render_path") or "").strip(),
|
||||
render_digest=str(data.get("render_digest") or "").strip(),
|
||||
changed=bool(data.get("changed", False)),
|
||||
valid=bool(data.get("valid", False)),
|
||||
errors=errors,
|
||||
warnings=warnings,
|
||||
diff=self._parse_singbox_profile_diff(data.get("diff")),
|
||||
config=cast(Dict[str, Any], raw_config),
|
||||
)
|
||||
|
||||
def _parse_singbox_profile_rollback(
|
||||
self,
|
||||
data: Dict[str, Any],
|
||||
*,
|
||||
fallback_id: str = "",
|
||||
fallback_client: str = "",
|
||||
) -> SingBoxProfileRollbackResult:
|
||||
return SingBoxProfileRollbackResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=str(data.get("message") or "").strip(),
|
||||
code=str(data.get("code") or "").strip(),
|
||||
profile_id=str(data.get("profile_id") or fallback_id).strip(),
|
||||
client_id=str(data.get("client_id") or fallback_client).strip(),
|
||||
config_path=str(data.get("config_path") or "").strip(),
|
||||
history_id=str(data.get("history_id") or "").strip(),
|
||||
profile_revision=self._to_int(data.get("profile_revision")),
|
||||
last_applied_at=str(data.get("last_applied_at") or "").strip(),
|
||||
)
|
||||
|
||||
def _parse_singbox_profile_history_entry(self, raw: Any) -> Optional[SingBoxProfileHistoryEntry]:
|
||||
if not isinstance(raw, dict):
|
||||
return None
|
||||
hid = str(raw.get("id") or "").strip()
|
||||
if not hid:
|
||||
return None
|
||||
return SingBoxProfileHistoryEntry(
|
||||
id=hid,
|
||||
at=str(raw.get("at") or "").strip(),
|
||||
profile_id=str(raw.get("profile_id") or "").strip(),
|
||||
action=str(raw.get("action") or "").strip().lower(),
|
||||
status=str(raw.get("status") or "").strip().lower(),
|
||||
code=str(raw.get("code") or "").strip(),
|
||||
message=str(raw.get("message") or "").strip(),
|
||||
profile_revision=self._to_int(raw.get("profile_revision")),
|
||||
render_revision=self._to_int(raw.get("render_revision")),
|
||||
render_digest=str(raw.get("render_digest") or "").strip(),
|
||||
render_path=str(raw.get("render_path") or "").strip(),
|
||||
client_id=str(raw.get("client_id") or "").strip(),
|
||||
)
|
||||
|
||||
def _parse_singbox_profile_history(
|
||||
self,
|
||||
data: Dict[str, Any],
|
||||
*,
|
||||
fallback_id: str = "",
|
||||
) -> SingBoxProfileHistoryResult:
|
||||
raw_items = data.get("items") or []
|
||||
if not isinstance(raw_items, list):
|
||||
raw_items = []
|
||||
items: List[SingBoxProfileHistoryEntry] = []
|
||||
for row in raw_items:
|
||||
entry = self._parse_singbox_profile_history_entry(row)
|
||||
if entry is not None:
|
||||
items.append(entry)
|
||||
|
||||
return SingBoxProfileHistoryResult(
|
||||
ok=bool(data.get("ok", False)),
|
||||
message=str(data.get("message") or "").strip(),
|
||||
code=str(data.get("code") or "").strip(),
|
||||
profile_id=str(data.get("profile_id") or fallback_id).strip(),
|
||||
count=self._to_int(data.get("count"), default=len(items)),
|
||||
items=items,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user