bocai-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from bocai_cli.api import ApiFacade
6
+ from bocai_cli.command_utils import command_handler
7
+ from bocai_cli.config import load_config
8
+ from bocai_cli.date_range_validation import validate_optional_date_range
9
+ from bocai_cli.output import print_json
10
+
11
+ app = typer.Typer(help="report相关命令。", invoke_without_command=True)
12
+
13
+
14
+ @app.callback()
15
+ def report_root(ctx: typer.Context) -> None:
16
+ if ctx.invoked_subcommand is None:
17
+ typer.echo(ctx.get_help())
18
+ raise typer.Exit(code=0)
19
+
20
+
21
+ def _build_api() -> ApiFacade:
22
+ config = load_config()
23
+ return ApiFacade(
24
+ base_url=config.base_url,
25
+ token=config.token,
26
+ timeout_seconds=config.timeout_seconds,
27
+ )
28
+
29
+
30
+ @app.command("revenue", help="获取营收数据")
31
+ @command_handler
32
+ def revenue(
33
+ start_date: str | None = typer.Option(
34
+ None,
35
+ "--start-date",
36
+ help="开始日期,格式 YYYY-MM-DD。",
37
+ ),
38
+ end_date: str | None = typer.Option(
39
+ None,
40
+ "--end-date",
41
+ help="结束日期,格式 YYYY-MM-DD。",
42
+ ),
43
+ center_id: str | None = typer.Option(
44
+ None,
45
+ "--center-id",
46
+ help="门店ID。",
47
+ ),
48
+ ) -> None:
49
+ validate_optional_date_range(start_date, end_date)
50
+
51
+ api = _build_api()
52
+ data = api.report.get_revenue(
53
+ start_date=start_date,
54
+ end_date=end_date,
55
+ center_id=center_id,
56
+ )
57
+ print_json({"status": "ok", "data": data})
58
+
59
+
60
+ @app.command("revenue-detail", help="获取营收明细数据")
61
+ @command_handler
62
+ def revenue_detail(
63
+ detail_type: str = typer.Option(
64
+ ...,
65
+ "--type",
66
+ help="营收明细类型。",
67
+ ),
68
+ start_date: str | None = typer.Option(
69
+ None,
70
+ "--start-date",
71
+ help="开始日期,格式 YYYY-MM-DD。",
72
+ ),
73
+ end_date: str | None = typer.Option(
74
+ None,
75
+ "--end-date",
76
+ help="结束日期,格式 YYYY-MM-DD。",
77
+ ),
78
+ center_id: str | None = typer.Option(
79
+ None,
80
+ "--center-id",
81
+ help="门店ID。",
82
+ ),
83
+ ) -> None:
84
+ validate_optional_date_range(start_date, end_date)
85
+
86
+ api = _build_api()
87
+ data = api.report.get_revenue_detail(
88
+ type_=detail_type,
89
+ start_date=start_date,
90
+ end_date=end_date,
91
+ center_id=center_id,
92
+ )
93
+ print_json({"status": "ok", "data": data})
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from bocai_cli.api import ApiFacade
6
+ from bocai_cli.command_utils import command_handler
7
+ from bocai_cli.config import load_config
8
+ from bocai_cli.date_range_validation import validate_date_range
9
+ from bocai_cli.output import print_json
10
+
11
+ app = typer.Typer(help="预约相关命令。", invoke_without_command=True)
12
+
13
+
14
+ @app.callback()
15
+ def reservation_root(ctx: typer.Context) -> None:
16
+ if ctx.invoked_subcommand is None:
17
+ typer.echo(ctx.get_help())
18
+ raise typer.Exit(code=0)
19
+
20
+
21
+ def _build_api() -> ApiFacade:
22
+ config = load_config()
23
+ return ApiFacade(
24
+ base_url=config.base_url,
25
+ token=config.token,
26
+ timeout_seconds=config.timeout_seconds,
27
+ )
28
+
29
+
30
+ @app.command("list", help="获取预约记录数据")
31
+ @command_handler
32
+ def list_reservation(
33
+ start_date: str = typer.Option(
34
+ ...,
35
+ "--start-date",
36
+ help="开始日期,格式 YYYY-MM-DD。",
37
+ ),
38
+ end_date: str = typer.Option(
39
+ ...,
40
+ "--end-date",
41
+ help="结束日期,格式 YYYY-MM-DD。",
42
+ ),
43
+ page_num: int = typer.Option(
44
+ ...,
45
+ "--page-num",
46
+ min=0,
47
+ help="页码,必须大于等于 0。",
48
+ ),
49
+ page_size: int = typer.Option(
50
+ ...,
51
+ "--page-size",
52
+ min=1,
53
+ help="每页条数,必须大于等于 1。",
54
+ ),
55
+ center_id: str | None = typer.Option(
56
+ None,
57
+ "--center-id",
58
+ help="门店ID。",
59
+ ),
60
+ ) -> None:
61
+ validate_date_range(start_date, end_date)
62
+
63
+ api = _build_api()
64
+ data = api.reservation.list(
65
+ start_date=start_date,
66
+ end_date=end_date,
67
+ page_num=page_num,
68
+ page_size=page_size,
69
+ center_id=center_id,
70
+ )
71
+ print_json({"status": "ok", "data": data})
bocai_cli/config.py ADDED
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import tomli_w
10
+
11
+ from bocai_cli.errors import ConfigValidationError
12
+
13
+ try:
14
+ import tomllib
15
+ except ModuleNotFoundError: # pragma: no cover
16
+ import tomli as tomllib # type: ignore[no-redef]
17
+
18
+ DEFAULT_CONFIG_PATH = Path.home() / ".config" / "bocai-cli" / "config.toml"
19
+
20
+
21
+ @dataclass
22
+ class AppConfig:
23
+ base_url: str | None = None
24
+ token: str | None = None
25
+ timeout_seconds: float = 30.0
26
+ app_id: str | None = None
27
+ secret: str | None = None
28
+ token_url: str | None = None
29
+ token_backup: str | None = None
30
+ token_updated_at: str | None = None
31
+
32
+
33
+ def get_config_path() -> Path:
34
+ raw_path = os.getenv("BOCAI_CLI_CONFIG_PATH")
35
+ if raw_path:
36
+ return Path(raw_path).expanduser()
37
+ return DEFAULT_CONFIG_PATH
38
+
39
+
40
+ def load_config(path: Path | None = None) -> AppConfig:
41
+ config_path = path or get_config_path()
42
+ if not config_path.exists():
43
+ return AppConfig()
44
+
45
+ try:
46
+ with config_path.open("rb") as f:
47
+ data = tomllib.load(f)
48
+ except tomllib.TOMLDecodeError as exc:
49
+ raise ConfigValidationError(
50
+ "配置文件格式错误,请检查 TOML 语法。",
51
+ details={"config_path": str(config_path)},
52
+ ) from exc
53
+ except OSError as exc:
54
+ raise ConfigValidationError(
55
+ "配置文件读取失败,请检查文件权限或路径。",
56
+ details={"config_path": str(config_path)},
57
+ ) from exc
58
+
59
+ return AppConfig(
60
+ base_url=_optional_string(data.get("base_url")),
61
+ token=_optional_string(data.get("token")),
62
+ timeout_seconds=_optional_float(data.get("timeout_seconds"), default=30.0),
63
+ app_id=_optional_string(data.get("app_id")),
64
+ secret=_optional_string(data.get("secret")),
65
+ token_url=_optional_string(data.get("token_url")),
66
+ token_backup=_optional_string(data.get("token_backup")),
67
+ token_updated_at=_optional_string(data.get("token_updated_at")),
68
+ )
69
+
70
+
71
+ def save_config(config: AppConfig, path: Path | None = None) -> Path:
72
+ config_path = path or get_config_path()
73
+ config_path.parent.mkdir(parents=True, exist_ok=True)
74
+
75
+ payload = _to_toml_payload(config)
76
+ with config_path.open("wb") as f:
77
+ f.write(tomli_w.dumps(payload).encode("utf-8"))
78
+
79
+ _enforce_permissions_best_effort(config_path)
80
+ return config_path
81
+
82
+
83
+ def _to_toml_payload(config: AppConfig) -> dict[str, Any]:
84
+ values = asdict(config)
85
+ payload: dict[str, Any] = {}
86
+ for key, value in values.items():
87
+ if value is None:
88
+ continue
89
+ payload[key] = value
90
+ return payload
91
+
92
+
93
+ def _optional_string(raw: Any) -> str | None:
94
+ if raw is None:
95
+ return None
96
+ value = str(raw).strip()
97
+ return value or None
98
+
99
+
100
+ def _optional_float(raw: Any, default: float) -> float:
101
+ if raw is None:
102
+ return default
103
+ try:
104
+ return float(raw)
105
+ except (TypeError, ValueError):
106
+ return default
107
+
108
+
109
+ def _enforce_permissions_best_effort(path: Path) -> None:
110
+ if sys.platform.startswith("win"):
111
+ return
112
+
113
+ try:
114
+ path.chmod(0o600)
115
+ except OSError:
116
+ return
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date, datetime
4
+
5
+ from bocai_cli.errors import ConfigValidationError
6
+
7
+
8
+ def _parse_date(value: str, *, option_name: str) -> date:
9
+ try:
10
+ return datetime.strptime(value, "%Y-%m-%d").date()
11
+ except ValueError as exc:
12
+ raise ConfigValidationError(
13
+ f"--{option_name} 格式必须为 YYYY-MM-DD。",
14
+ details={"invalid": [option_name]},
15
+ ) from exc
16
+
17
+
18
+ def validate_date_range(start_date: str, end_date: str) -> None:
19
+ start = _parse_date(start_date, option_name="start-date")
20
+ end = _parse_date(end_date, option_name="end-date")
21
+ if start > end:
22
+ raise ConfigValidationError(
23
+ "--start-date 不能晚于 --end-date。",
24
+ details={"start_date": start_date, "end_date": end_date},
25
+ )
26
+
27
+ range_days = (end - start).days + 1
28
+ if range_days > 31:
29
+ raise ConfigValidationError(
30
+ "--start-date 到 --end-date 的时间范围不能超过 31 天。",
31
+ details={
32
+ "start_date": start_date,
33
+ "end_date": end_date,
34
+ "range_days": range_days,
35
+ },
36
+ )
37
+
38
+
39
+ def validate_optional_date_range(start_date: str | None, end_date: str | None) -> None:
40
+ if (start_date is None) != (end_date is None):
41
+ raise ConfigValidationError("--start-date 和 --end-date 必须同时提供。")
42
+
43
+ if start_date is None and end_date is None:
44
+ return
45
+
46
+ assert start_date is not None
47
+ assert end_date is not None
48
+ validate_date_range(start_date, end_date)
bocai_cli/errors.py ADDED
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class BocaiCliError(Exception):
9
+ message: str
10
+ status_code: int | None = None
11
+ details: dict[str, Any] = field(default_factory=dict)
12
+
13
+ def __str__(self) -> str:
14
+ return self.message
15
+
16
+
17
+ class ConfigValidationError(BocaiCliError):
18
+ pass
19
+
20
+
21
+ class ApiRequestError(BocaiCliError):
22
+ pass
bocai_cli/main.py ADDED
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from bocai_cli.commands.registry import register_commands
6
+
7
+ app = typer.Typer(no_args_is_help=True, help="菠菜SAAS CLI")
8
+ register_commands(app)
9
+
10
+ if __name__ == "__main__":
11
+ app()
bocai_cli/output.py ADDED
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ import typer
7
+
8
+
9
+ def print_json(payload: dict[str, Any]) -> None:
10
+ typer.echo(json.dumps(payload, ensure_ascii=False))
11
+
12
+
13
+ def print_error(
14
+ message: str,
15
+ *,
16
+ status_code: int | None = None,
17
+ details: dict[str, Any] | None = None,
18
+ ) -> None:
19
+ error_payload: dict[str, Any] = {
20
+ "status": "error",
21
+ "message": message,
22
+ }
23
+ if status_code is not None:
24
+ error_payload["status_code"] = status_code
25
+ if details:
26
+ error_payload["details"] = details
27
+
28
+ print_json(error_payload)
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: bocai-cli
3
+ Version: 0.1.0
4
+ Summary: CLI wrapper for SaaS APIs
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx>=0.27.0
7
+ Requires-Dist: tenacity>=9.0.0
8
+ Requires-Dist: tomli-w>=1.0.0
9
+ Requires-Dist: typer>=0.12.3
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.2.0; extra == 'dev'
12
+ Requires-Dist: respx>=0.21.1; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # bocai-cli
16
+
17
+ 使用 Python + Typer 封装 SaaS API 的命令行工具。
18
+
19
+ ## 安装
20
+
21
+ ### 1) 本地路径安装
22
+
23
+ ```bash
24
+ python -m pip install .
25
+ ```
26
+
27
+ ### 2) 可编辑安装(开发)
28
+
29
+ ```bash
30
+ python -m pip install -e .
31
+ ```
32
+
33
+ ### 3) 私有 Git 安装
34
+
35
+ ```bash
36
+ python -m pip install "git+<repo-url>@<tag-or-commit>"
37
+ ```
38
+
39
+ ### 4)使用构建生成的wheel安装
40
+ ```bash
41
+ pip install xxx.whl(e.g. bocai_cli-0.1.0-py3-none-any.whl)
42
+ ```
43
+
44
+ ## 构建 wheel(私仓分发),同时打包依赖
45
+
46
+ ```bash
47
+ python -m pip wheel . -w dist
48
+ ```
49
+
50
+ 构建后会在 `dist/` 目录下生成类似 `bocai_cli-0.1.0-py3-none-any.whl` 的文件。
51
+
52
+
53
+ ## 构建 wheel(私仓分发),不打包依赖
54
+
55
+ ```bash
56
+ python -m pip wheel . --no-deps -w dist
57
+ ```
58
+
59
+ 构建后会在 `dist/` 目录下生成类似 `bocai_cli-0.1.0-py3-none-any.whl` 的文件。
60
+
61
+ ## 安装后验证
62
+
63
+ ```bash
64
+ bocai-cli --help
65
+ bocai-cli auth --help
66
+ bocai-cli report --help
67
+ ```
68
+
69
+ ## 配置文件
70
+ ### Windows系统
71
+ C:\Users\{用户名}\.config\bocai-cli\config.toml
72
+ ### Linux系统
73
+ #### 先读环境变量 BOCAI_CLI_CONFIG_PATH
74
+ #### 没有就读默认路径:~/.config/bocai-cli/config.toml
75
+ /home/{用户名}/.config/bocai-cli\config.toml (非root用户)
76
+
77
+ /root/.config/bocai-cli\config.toml (root用户)
78
+ ### config.toml配置内容(base_url,app_id,secret按实际替换)
79
+ base_url = "http://10.0.0.227:10086"
80
+
81
+ app_id = "test_app_001"
82
+
83
+ secret = "test_secret_123456"
@@ -0,0 +1,29 @@
1
+ bocai_cli/__init__.py,sha256=4t_crzhrLum--oyowUMxtjBTzUtWp7oRTF22ewEvJG4,49
2
+ bocai_cli/client.py,sha256=xjtirUDW9ip3kHAhKaTnaXiqtZaFMqx81Yx5KFQogzs,1754
3
+ bocai_cli/command_utils.py,sha256=JlE_fh_cWRpEnoUamNLrZSEm-GOWy-QdiXHDW3Xgt9c,764
4
+ bocai_cli/config.py,sha256=f5DZ_zd7hkLZfNjzqrdZvg2HVO94hZZJ3GwH4TXVWyI,3254
5
+ bocai_cli/date_range_validation.py,sha256=5mKA3I13-VmdNrHJTz4tpf902tqrwVumS6tWEUvadqM,1584
6
+ bocai_cli/errors.py,sha256=1r9104aIibAmGIIGOBwwggZNPl-PpT-6mZo-wMi5qa0,418
7
+ bocai_cli/main.py,sha256=VjiHlam7LeX9OKbM8H3_Wye_iqcu-XO2n2sMexhUWW8,234
8
+ bocai_cli/output.py,sha256=c-kvmVpuTxY-CEVuRv93nyEGy5Tu-jPGsrfuHKd3F4o,598
9
+ bocai_cli/api/__init__.py,sha256=72HnDON6aRw-hcNCDkeUYvCtcKG_VAiAR82h39kd-dE,588
10
+ bocai_cli/api/auth_client.py,sha256=Oec2H4C1ofQKS9ePFVy-M9nYm7ZBzjuoQmAYy2em0U0,830
11
+ bocai_cli/api/card_client.py,sha256=HqBvpBop9T2phJtglR0Z5RPOeHxaGltRU16LPBit7r0,2294
12
+ bocai_cli/api/checkin_client.py,sha256=H620MXbPtc_06ncVuQgj7ksh2ZkiTUEntxWZeb0J_ts,755
13
+ bocai_cli/api/course_client.py,sha256=dDx-a-SBx2ilUcDudqqh4BTmCUa0oFkjlokj0_l3dCo,2300
14
+ bocai_cli/api/facade.py,sha256=-ok7YPlOnDMAYZJCw5svOjqS6RJANeoi0z6giJGvWPs,1040
15
+ bocai_cli/api/report_client.py,sha256=pQQ71_gIowMH3AX9cVnOd3UZWpgf5gpoNKhkx-2qZCY,1343
16
+ bocai_cli/api/reservation_client.py,sha256=4DmgazRQIPLtuvxNR_p-QUyzexk6NMemtuXz3uX4wDw,763
17
+ bocai_cli/api/transport.py,sha256=BAJWm5dlPpl7lOPaxj2V1ebXlj_VX2ostJMiPn13Ss0,5368
18
+ bocai_cli/commands/__init__.py,sha256=gAOyyeRGRJiAV0sL3in9-ETCvJCDVUIvXbgsGJ3RsEE,374
19
+ bocai_cli/commands/auth.py,sha256=EL9KKZ15Lw9Fa5IEFiEJB4fqcjwlksRCXmGqt6kDCDs,1877
20
+ bocai_cli/commands/card.py,sha256=vzELiduvzmh8zT25qDvwuPMsr6eZNsgwaCcjGMev-ac,7764
21
+ bocai_cli/commands/checkin.py,sha256=Okj76j0BUv1sK-ygTpsprXxeIogqjMbCMUNFSsEQS-A,1789
22
+ bocai_cli/commands/course.py,sha256=72Cor9pwa4DRdKGL0NR9qwij0PWp6T5rtoN4Ld6TQ1M,7810
23
+ bocai_cli/commands/registry.py,sha256=BpKsCkn3cHXgmULGFuksu8MfQZfTAIJePKK0Cg179cA,1230
24
+ bocai_cli/commands/report.py,sha256=_weEShAsRO8PT3LewHQs2-xATMnOgWlpQW-Mxe3FsWM,2381
25
+ bocai_cli/commands/reservation.py,sha256=O7a9o2Obci1RZdlh_2X2i6vqQICwGfahAJOlxRRSZu0,1801
26
+ bocai_cli-0.1.0.dist-info/METADATA,sha256=0ojRQBqUmmqQtsVculD5IL1L5iHd2G8FXuKiqiY33AE,1798
27
+ bocai_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
28
+ bocai_cli-0.1.0.dist-info/entry_points.txt,sha256=iaKWpXFc8niVX-4Eimhpzwq3R11pfFxjpZS4RFJLpEU,49
29
+ bocai_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bocai-cli = bocai_cli.main:app