feat(api): standardize REST contracts and auth

This commit is contained in:
2026-07-30 20:38:51 +08:00
parent ae1a657554
commit ba947b616b
86 changed files with 54193 additions and 990 deletions
+23 -59
View File
@@ -17,8 +17,6 @@ SCHEMA_VERSION = "tjwater-cli/v1"
CLI_NAME = "tjwater-cli"
DEFAULT_TIMEOUT = 180
DEFAULT_SERVER = "http://192.168.1.114:8000"
class CLIError(Exception):
def __init__(
self,
@@ -46,8 +44,6 @@ class AuthContext:
server: str | None = None
access_token: str | None = None
project_id: str | None = None
username: str | None = None
network: str | None = None
headers: dict[str, str] = field(default_factory=dict)
@@ -97,8 +93,6 @@ def load_auth_context(auth_stdin: bool = False) -> AuthContext:
"server": os.getenv("TJWATER_SERVER"),
"access_token": os.getenv("TJWATER_ACCESS_TOKEN"),
"project_id": os.getenv("TJWATER_PROJECT_ID"),
"username": os.getenv("TJWATER_USERNAME"),
"network": os.getenv("TJWATER_NETWORK"),
"headers": json.loads(extra_headers) if extra_headers else {},
}
@@ -115,8 +109,6 @@ def load_auth_context(auth_stdin: bool = False) -> AuthContext:
server=_pick(raw, "server", "base_url"),
access_token=_pick(raw, "access_token", "token", "accessToken"),
project_id=_pick(raw, "project_id", "projectId", "x_project_id"),
username=_pick(raw, "username", "preferred_username"),
network=_pick(raw, "network", "project_code", "projectCode", "project"),
headers={str(key): str(value) for key, value in headers.items()},
)
@@ -175,30 +167,6 @@ def require_project_id(ctx: RuntimeContext) -> str:
)
def require_network(ctx: RuntimeContext) -> str:
if ctx.auth.network:
return ctx.auth.network
raise CLIError(
"认证失败",
code="NETWORK_CONTEXT_REQUIRED",
message="missing network in auth context for legacy network-based endpoints",
exit_code=3,
next_commands=["add network to auth context"],
)
def require_username(ctx: RuntimeContext) -> str:
if ctx.auth.username:
return ctx.auth.username
raise CLIError(
"认证失败",
code="USERNAME_CONTEXT_REQUIRED",
message="missing username in auth context",
exit_code=3,
next_commands=["add username to auth context"],
)
def resolve_scheme(ctx: RuntimeContext, explicit_scheme: str | None, *, required: bool = False) -> str | None:
scheme = explicit_scheme or ctx.scheme
if required and not scheme:
@@ -254,14 +222,14 @@ def parse_burst_file(path: Path) -> tuple[list[str], list[float]]:
raw = read_json_input(path, label="burst")
if isinstance(raw, dict) and "bursts" in raw:
raw = raw["bursts"]
if isinstance(raw, dict) and "burst_ID" in raw and "burst_size" in raw:
ids = [str(item) for item in raw["burst_ID"]]
if isinstance(raw, dict) and "burst_id" in raw and "burst_size" in raw:
ids = [str(item) for item in raw["burst_id"]]
sizes = [float(item) for item in raw["burst_size"]]
if len(ids) != len(sizes):
raise CLIError(
"CLI 参数错误",
code="BURST_FILE_INVALID",
message="burst file burst_ID and burst_size must have the same length",
message="burst file burst_id and burst_size must have the same length",
exit_code=2,
)
return ids, sizes
@@ -282,7 +250,7 @@ def parse_burst_file(path: Path) -> tuple[list[str], list[float]]:
raise CLIError(
"CLI 参数错误",
code="BURST_FILE_INVALID",
message="burst file must be a JSON array or object with burst_ID/burst_size",
message="burst file must be a JSON array or object with burst_id/burst_size",
exit_code=2,
)
@@ -404,12 +372,13 @@ def _parse_response_body(response: requests.Response) -> Any:
return {}
def _with_network_param(params: dict[str, Any] | None, network: str) -> dict[str, Any]:
params = dict(params or {})
if "network" in params or "network_name" in params or "name" in params:
return params
params["network"] = network
return params
def _prepare_public_request(
method: str,
path: str,
params: dict[str, Any] | None,
json_body: Any,
) -> tuple[str, str, dict[str, Any] | None, Any]:
return method.upper(), path.rstrip("/") or "/", params or None, json_body
def request_json(
@@ -421,18 +390,14 @@ def request_json(
json_body: Any = None,
require_auth: bool = True,
require_project: bool = False,
require_network_ctx: bool = False,
require_username_ctx: bool = False,
) -> tuple[Any, int]:
require_server(ctx)
network = None
if require_network_ctx:
network = require_network(ctx)
if require_username_ctx:
require_username(ctx)
if network and (params is not None or json_body is None):
params = _with_network_param(params, network)
method, path, params, json_body = _prepare_public_request(
method,
path,
params,
json_body,
)
url = f"{require_server(ctx)}/api/v1{path}"
headers = build_headers(ctx, require_auth=require_auth, require_project=require_project)
started = time.monotonic()
@@ -482,15 +447,14 @@ def request_bytes(
params: dict[str, Any] | None = None,
require_auth: bool = True,
require_project: bool = False,
require_network_ctx: bool = False,
) -> tuple[bytes, int]:
require_server(ctx)
network = None
if require_network_ctx:
network = require_network(ctx)
if network:
params = _with_network_param(params, network)
method, path, params, _ = _prepare_public_request(
method,
path,
params,
None,
)
url = f"{require_server(ctx)}/api/v1{path}"
headers = build_headers(ctx, require_auth=require_auth, require_project=require_project)
started = time.monotonic()