Skip to content

Commit 23675e0

Browse files
committed
account info
1 parent 00df969 commit 23675e0

9 files changed

Lines changed: 182 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ pip install 115cli
2323
After [authenticating](#authentication) with `115cli auth`, you can use the `115cli` command to interact with your 115 cloud storage. Here are some examples of available commands:
2424

2525
```bash
26+
# Account info
27+
115cli account
28+
2629
# List files
2730
115cli ls /
2831
115cli ls /path/to/dir -l

README.zh.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ pip install 115cli
2323
常见示例:
2424

2525
```bash
26+
# 账户信息
27+
115cli account
28+
2629
# 目录
2730
115cli ls /
2831
115cli ls /path/to/dir -l

cli115/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55

66
from cli115.cmds.base import BaseCommand
7+
from cli115.cmds.account import AccountCommand
78
from cli115.cmds.auth import AuthCommand
89
from cli115.cmds.config_cmd import ConfigCommand
910
from cli115.cmds.cp import CpCommand
@@ -19,6 +20,7 @@
1920
from cli115.cmds.upload import UploadCommand
2021

2122
COMMANDS: dict[str, BaseCommand] = {
23+
"account": AccountCommand(),
2224
"auth": AuthCommand(),
2325
"config": ConfigCommand(),
2426
"ls": LsCommand(),

cli115/client/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from cli115.client.base import (
2+
AccountClient,
23
Client,
34
Directory,
45
DownloadClient,
@@ -12,6 +13,7 @@
1213
from cli115.client.factory import create_client
1314

1415
__all__ = [
16+
"AccountClient",
1517
"Client",
1618
"Directory",
1719
"DownloadClient",

cli115/client/base.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,31 @@ class DownloadInfo:
249249
cookies: str
250250

251251

252+
@dataclass(frozen=True)
253+
class AccountInfo:
254+
"""Account information for the authenticated user.
255+
256+
Attributes:
257+
user_name: Display name of the user.
258+
user_id: Numeric user ID.
259+
vip: Whether the user currently has VIP status.
260+
expire: VIP expiry datetime, or ``None`` if not available.
261+
"""
262+
263+
user_name: str
264+
user_id: int
265+
vip: bool
266+
expire: datetime | None
267+
268+
252269
class Client(ABC):
253270
"""Abstract high-level client interface."""
254271

272+
@property
273+
@abstractmethod
274+
def account(self) -> AccountClient:
275+
"""Access account operations."""
276+
255277
@property
256278
@abstractmethod
257279
def file(self) -> FileClient:
@@ -536,3 +558,15 @@ def delete(self, *task_hashes: str) -> None:
536558
Args:
537559
task_hashes: info_hash values of tasks to delete.
538560
"""
561+
562+
563+
class AccountClient(ABC):
564+
"""Abstract interface for account operations."""
565+
566+
@abstractmethod
567+
def info(self) -> AccountInfo:
568+
"""Get account information for the authenticated user.
569+
570+
Returns:
571+
An AccountInfo with user name, user ID, VIP status and expiry.
572+
"""

cli115/client/default.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import os
66
import warnings
7+
from datetime import datetime
78
from os import PathLike
89
from property import locked_cacheproperty
910
from typing import BinaryIO, Callable
@@ -13,6 +14,8 @@
1314

1415
from cli115.auth.base import Auth
1516
from cli115.client.base import (
17+
AccountClient,
18+
AccountInfo,
1619
Client,
1720
CloudTask,
1821
DEFAULT_PAGE_SIZE,
@@ -101,14 +104,38 @@ def _is_waf_blocked(exc: HTTPStatusError) -> bool:
101104
return "aliyun.com" in body_text or "alicdn.com" in body_text
102105

103106

107+
class DefaultAccountClient(AccountClient):
108+
109+
def __init__(self, client: DefaultClient):
110+
self._client = client
111+
112+
def info(self) -> AccountInfo:
113+
resp = self._client._api.user_my()
114+
check_response(resp)
115+
data = resp.get("data", {})
116+
expire_ts = data.get("expire")
117+
expire = datetime.fromtimestamp(expire_ts) if expire_ts else None
118+
return AccountInfo(
119+
user_name=data.get("user_name", ""),
120+
user_id=int(data.get("user_id", 0)),
121+
vip=bool(data.get("vip", 0)),
122+
expire=expire,
123+
)
124+
125+
104126
class DefaultClient(Client):
105127

106128
def __init__(self, auth: Auth):
107129
self._auth = auth
108130
self._api = _115Client(auth.get_cookies())
131+
self._account = DefaultAccountClient(self)
109132
self._file = DefaultFileClient(self)
110133
self._download = DefaultDownloadClient(self)
111134

135+
@property
136+
def account(self) -> AccountClient:
137+
return self._account
138+
112139
@property
113140
def file(self) -> FileClient:
114141
return self._file

cli115/cmds/account.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Account command – shows info for the authenticated account."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import sys
7+
8+
from cli115.cmds.base import BaseCommand
9+
from cli115.cmds.formatter import PairFormatterMixin
10+
11+
12+
class AccountCommand(PairFormatterMixin, BaseCommand):
13+
"""Show account info."""
14+
15+
def register(self, parser: argparse.ArgumentParser) -> None:
16+
super().register(parser)
17+
18+
def execute(self, args: argparse.Namespace) -> None:
19+
client = self._create_client()
20+
21+
try:
22+
info = client.account.info()
23+
except Exception as e:
24+
print(f"Error: {e}", file=sys.stderr)
25+
sys.exit(1)
26+
27+
pairs = [
28+
("Username", info.user_name),
29+
("User ID", info.user_id),
30+
("VIP", info.vip),
31+
("Expire", info.expire),
32+
]
33+
self.output(pairs, args)

tests/test_cli_commands.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import configparser
77

8+
from cli115.cmds.account import AccountCommand
89
from cli115.cmds.auth import AuthCommand, _parse_cookie_string
910
from cli115.cmds.config_cmd import ConfigCommand
1011
from cli115.cmds.cp import CpCommand
@@ -26,6 +27,7 @@
2627
from cli115.cmds.upload import UploadCommand
2728
from cli115.cli import build_parser, main
2829
from cli115.client.base import (
30+
AccountInfo,
2931
CloudTask,
3032
Directory,
3133
DownloadInfo,
@@ -1092,5 +1094,66 @@ def test_config_command_registered_in_cli(self, mock_load):
10921094
self.assertIn("[general]", mock_out.getvalue())
10931095

10941096

1097+
class TestAccountCommand(unittest.TestCase):
1098+
1099+
@patch("cli115.cmds.account.BaseCommand._create_client")
1100+
def test_execute_prints_account_info(self, mock_create):
1101+
mock_client = MagicMock()
1102+
mock_client.account.info.return_value = AccountInfo(
1103+
user_name="testuser",
1104+
user_id=12345,
1105+
vip=True,
1106+
expire=datetime(2025, 1, 1),
1107+
)
1108+
mock_create.return_value = mock_client
1109+
1110+
cmd = AccountCommand()
1111+
import argparse
1112+
1113+
args = argparse.Namespace(format="plain")
1114+
with patch("builtins.print") as mock_print:
1115+
cmd.execute(args)
1116+
output = mock_print.call_args[0][0]
1117+
self.assertIn("testuser", output)
1118+
self.assertIn("12345", output)
1119+
1120+
@patch("cli115.cmds.account.BaseCommand._create_client")
1121+
def test_execute_json_format(self, mock_create):
1122+
mock_client = MagicMock()
1123+
mock_client.account.info.return_value = AccountInfo(
1124+
user_name="jsonuser",
1125+
user_id=99,
1126+
vip=False,
1127+
expire=None,
1128+
)
1129+
mock_create.return_value = mock_client
1130+
1131+
cmd = AccountCommand()
1132+
import argparse
1133+
1134+
args = argparse.Namespace(format="json")
1135+
with patch("builtins.print") as mock_print:
1136+
cmd.execute(args)
1137+
import json
1138+
1139+
output = json.loads(mock_print.call_args[0][0])
1140+
self.assertEqual(output["Username"], "jsonuser")
1141+
self.assertEqual(output["User ID"], 99)
1142+
1143+
@patch("cli115.cmds.account.BaseCommand._create_client")
1144+
def test_execute_error_exits(self, mock_create):
1145+
mock_client = MagicMock()
1146+
mock_client.account.info.side_effect = RuntimeError("API error")
1147+
mock_create.return_value = mock_client
1148+
1149+
cmd = AccountCommand()
1150+
import argparse
1151+
1152+
args = argparse.Namespace(format="plain")
1153+
with self.assertRaises(SystemExit) as ctx:
1154+
cmd.execute(args)
1155+
self.assertEqual(ctx.exception.code, 1)
1156+
1157+
10951158
if __name__ == "__main__":
10961159
unittest.main()

tests/test_client_account.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import unittest
2+
3+
from tests.base import BaseTestCase
4+
5+
6+
class TestDefaultAccountClient(BaseTestCase):
7+
8+
def test_info(self):
9+
info = self.client.account.info()
10+
self.assertTrue(info.user_id)
11+
self.assertTrue(info.user_name)
12+
13+
14+
if __name__ == "__main__":
15+
unittest.main()

0 commit comments

Comments
 (0)