paykka-kit 26.7.16__tar.gz
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.
- paykka_kit-26.7.16/.gitignore +24 -0
- paykka_kit-26.7.16/.mise.toml +38 -0
- paykka_kit-26.7.16/.python-version +1 -0
- paykka_kit-26.7.16/AGENTS.md +113 -0
- paykka_kit-26.7.16/CLAUDE.md +1 -0
- paykka_kit-26.7.16/PKG-INFO +16 -0
- paykka_kit-26.7.16/README.md +3 -0
- paykka_kit-26.7.16/paykka_kit/__init__.py +1 -0
- paykka_kit-26.7.16/paykka_kit/_version.py +1 -0
- paykka_kit-26.7.16/paykka_kit/core/__init__.py +1 -0
- paykka_kit-26.7.16/paykka_kit/core/config.py +52 -0
- paykka_kit-26.7.16/paykka_kit/core/console.py +20 -0
- paykka_kit-26.7.16/paykka_kit/core/exceptions.py +21 -0
- paykka_kit-26.7.16/paykka_kit/core/version.py +13 -0
- paykka_kit-26.7.16/paykka_kit/main.py +57 -0
- paykka_kit-26.7.16/paykka_kit/tools/__init__.py +1 -0
- paykka_kit-26.7.16/paykka_kit/tools/base.py +34 -0
- paykka_kit-26.7.16/paykka_kit/utils/__init__.py +1 -0
- paykka_kit-26.7.16/paykka_kit/utils/display.py +45 -0
- paykka_kit-26.7.16/pyproject.toml +28 -0
- paykka_kit-26.7.16/ruff.toml +9 -0
- paykka_kit-26.7.16/scripts/bump-version.py +55 -0
- paykka_kit-26.7.16/tests/__init__.py +1 -0
- paykka_kit-26.7.16/tests/conftest.py +12 -0
- paykka_kit-26.7.16/tests/test_core/__init__.py +1 -0
- paykka_kit-26.7.16/tests/test_core/test_config.py +87 -0
- paykka_kit-26.7.16/tests/test_core/test_console.py +55 -0
- paykka_kit-26.7.16/tests/test_core/test_display.py +95 -0
- paykka_kit-26.7.16/tests/test_core/test_exceptions.py +36 -0
- paykka_kit-26.7.16/tests/test_core/test_version.py +24 -0
- paykka_kit-26.7.16/tests/test_main.py +69 -0
- paykka_kit-26.7.16/tests/test_tools/__init__.py +1 -0
- paykka_kit-26.7.16/tests/test_tools/test_base.py +58 -0
- paykka_kit-26.7.16/uv.lock +249 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# IDE
|
|
14
|
+
.vscode/
|
|
15
|
+
.idea/
|
|
16
|
+
|
|
17
|
+
# OS
|
|
18
|
+
.DS_Store
|
|
19
|
+
Thumbs.db
|
|
20
|
+
|
|
21
|
+
# Coverage
|
|
22
|
+
htmlcov/
|
|
23
|
+
.coverage
|
|
24
|
+
coverage.xml
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[tools]
|
|
2
|
+
python = "3.13"
|
|
3
|
+
uv = "latest"
|
|
4
|
+
ruff = "latest"
|
|
5
|
+
|
|
6
|
+
[env]
|
|
7
|
+
PYTHONUNBUFFERED = "1"
|
|
8
|
+
|
|
9
|
+
[tasks.check]
|
|
10
|
+
description = "Run linter, formatter check, and type checker"
|
|
11
|
+
run = """
|
|
12
|
+
uv run ruff check . &&
|
|
13
|
+
uv run ruff format --check .
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
[tasks.fmt]
|
|
17
|
+
description = "Auto-format code"
|
|
18
|
+
run = "uv run ruff format . && uv run ruff check --fix ."
|
|
19
|
+
|
|
20
|
+
[tasks.test]
|
|
21
|
+
description = "Run unit tests with pytest and coverage"
|
|
22
|
+
run = "uv run pytest --cov=paykka_kit --cov-report=term-missing"
|
|
23
|
+
|
|
24
|
+
[tasks.build]
|
|
25
|
+
description = "Build distribution packages"
|
|
26
|
+
run = "uv build"
|
|
27
|
+
|
|
28
|
+
[tasks.publish]
|
|
29
|
+
description = "Build and publish to PyPI"
|
|
30
|
+
run = """
|
|
31
|
+
python scripts/bump-version.py &&
|
|
32
|
+
uv build &&
|
|
33
|
+
uv publish
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
[tasks.cli]
|
|
37
|
+
description = "Run paykka-kit CLI"
|
|
38
|
+
run = "uv run pk $@"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# AGENTS.md — paykka-kit AI 工作约束
|
|
2
|
+
|
|
3
|
+
## 项目定位
|
|
4
|
+
|
|
5
|
+
paykka-kit 是一个统一 CLI 工具集,将多个独立 CLI 工具整合到 `pk` 命令下。
|
|
6
|
+
分发名 `paykka-kit`,包名 `paykka_kit`。
|
|
7
|
+
|
|
8
|
+
## 技术栈
|
|
9
|
+
|
|
10
|
+
- Python 3.13、Typer(CLI 框架)、Rich(终端输出)
|
|
11
|
+
- 包管理 uv、工具链 mise、代码风格 ruff
|
|
12
|
+
- 测试 pytest、类型检查 pyrefly(https://pyrefly.org/)
|
|
13
|
+
|
|
14
|
+
## 开发铁律
|
|
15
|
+
|
|
16
|
+
1. **TDD 先行**:任何功能必须先写失败的测试,再写实现,最后重构。
|
|
17
|
+
|
|
18
|
+
正确流程:
|
|
19
|
+
- RED:写测试 → `mise test` 红
|
|
20
|
+
- GREEN:最小实现 → `mise test` 绿
|
|
21
|
+
- REFACTOR:重构代码 → `mise test` 仍绿
|
|
22
|
+
|
|
23
|
+
2. **提交前 `mise check`**:lint + format check + typecheck 必须全部通过。
|
|
24
|
+
|
|
25
|
+
3. **配置隔离**:paykka-kit 全局配置只管理自身行为,不涉足模块业务配置。
|
|
26
|
+
每个模块的配置以 `<module_name>.json` 独立管理,模块自行读写校验。
|
|
27
|
+
详见「配置文件路径」章节。
|
|
28
|
+
|
|
29
|
+
4. **异常体系**:所有异常继承 `PKError`(`core/exceptions.py`),使用退出码区分场景。
|
|
30
|
+
核心包所有公开类/函数以 `PK` 前缀命名(如 `PKError`、`PKTool`、`PKConfig`)。
|
|
31
|
+
|
|
32
|
+
5. **统一输出**:所有终端输出通过 `core/console.py` 的 Console 实例,禁止直接 `print()`。
|
|
33
|
+
|
|
34
|
+
6. **抽象优先**:新工具必须继承 `PKTool` 基类,实现 `name`/`help`/`register()`。
|
|
35
|
+
|
|
36
|
+
## 全局 Flags
|
|
37
|
+
|
|
38
|
+
| Flag | 短形式 | 作用 |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `--verbose` | `-v` | 输出详细调试信息 |
|
|
41
|
+
| `--output` | `-o` | 输出格式,支持 `json` / `table` |
|
|
42
|
+
| `--version` | `-V` | 输出版本号并退出 |
|
|
43
|
+
|
|
44
|
+
## 动态版本号
|
|
45
|
+
|
|
46
|
+
格式: `YY.M.D[.commit_short]`
|
|
47
|
+
|
|
48
|
+
- 基于构建当天日期,如 2026-07-16 → `26.7.16`
|
|
49
|
+
- 当日多次构建则追加 7 位 commit SHA:`26.7.16.a1b2c3d`
|
|
50
|
+
- 生成脚本:`scripts/bump-version.py`
|
|
51
|
+
|
|
52
|
+
## 命令速查
|
|
53
|
+
|
|
54
|
+
| 命令 | 作用 |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `mise check` | lint + format-check + typecheck |
|
|
57
|
+
| `mise fmt` | 自动格式化 |
|
|
58
|
+
| `mise test` | 跑单测 + 覆盖率 |
|
|
59
|
+
| `mise build` | 构建 wheel/sdist |
|
|
60
|
+
| `mise publish` | 发布到 PyPI |
|
|
61
|
+
| `mise cli` | 运行 CLI |
|
|
62
|
+
|
|
63
|
+
## 配置文件路径
|
|
64
|
+
|
|
65
|
+
遵循各平台约定,配置目录下以 `pk.json` 为全局配置、`<module_name>.json` 为模块专属配置。
|
|
66
|
+
|
|
67
|
+
| 平台 | 路径 |
|
|
68
|
+
|---|---|
|
|
69
|
+
| Unix(Linux / macOS) | `$XDG_CONFIG_HOME/pk/pk.json`,默认 `~/.config/pk/pk.json` |
|
|
70
|
+
| Windows | `%APPDATA%/pk/pk.json` |
|
|
71
|
+
|
|
72
|
+
## 项目结构约定
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
paykka-kit/
|
|
76
|
+
├── pyproject.toml
|
|
77
|
+
├── .mise.toml
|
|
78
|
+
├── .python-version # 3.13
|
|
79
|
+
├── ruff.toml
|
|
80
|
+
├── AGENTS.md # 本文档
|
|
81
|
+
├── CLAUDE.md -> AGENTS.md # 软链接
|
|
82
|
+
├── scripts/
|
|
83
|
+
│ └── bump-version.py # 动态版本生成脚本
|
|
84
|
+
├── paykka_kit/ # 源码根目录
|
|
85
|
+
│ ├── main.py # Typer 入口,全局 flags
|
|
86
|
+
│ ├── core/ # 核心抽象层
|
|
87
|
+
│ │ ├── version.py # get_version()
|
|
88
|
+
│ │ ├── config.py # PKConfig (JSON)
|
|
89
|
+
│ │ ├── console.py # Rich Console 统一输出
|
|
90
|
+
│ │ └── exceptions.py # PKError 异常体系
|
|
91
|
+
│ ├── tools/ # 工具模块
|
|
92
|
+
│ │ ├── base.py # PKTool ABC
|
|
93
|
+
│ │ └── <tool_name>/ # 每个工具独立子包
|
|
94
|
+
│ │ ├── cli.py # Typer 子命令组
|
|
95
|
+
│ │ ├── client.py # API 客户端
|
|
96
|
+
│ │ └── models.py # Pydantic 模型
|
|
97
|
+
│ └── utils/ # 共享工具函数
|
|
98
|
+
│ ├── http.py # httpx 封装
|
|
99
|
+
│ └── display.py # render_output (json/table)
|
|
100
|
+
└── tests/ # 测试,结构与源码一一对应
|
|
101
|
+
├── conftest.py
|
|
102
|
+
├── test_core/
|
|
103
|
+
└── test_tools/
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 编码风格
|
|
107
|
+
|
|
108
|
+
- 所有模块使用 `from __future__ import annotations`
|
|
109
|
+
- 类型注解覆盖率要求 100%
|
|
110
|
+
- 公开 API 必须有 docstring(Google 风格)
|
|
111
|
+
- 文件头不加 shebang(入口点由 pyproject.toml 的 scripts 声明)
|
|
112
|
+
- ruff 规则以 `ruff.toml` 为准,不在本文档重复
|
|
113
|
+
- 代码注释和 Git commit message 使用英文
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AGENTS.md
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paykka-kit
|
|
3
|
+
Version: 26.7.16
|
|
4
|
+
Summary: 统一 CLI 工具集
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: rich>=14.0
|
|
7
|
+
Requires-Dist: typer>=0.21.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest-cov>=7.0; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest>=9.0; extra == 'dev'
|
|
11
|
+
Requires-Dist: ruff>=0.12.0; extra == 'dev'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# paykka-kit
|
|
15
|
+
|
|
16
|
+
统一 CLI 工具集。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""""""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "26.7.16"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""""""
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""PKConfig — global config management following XDG / Windows APPDATA conventions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PKConfig:
|
|
12
|
+
"""paykka-kit global behaviour config; does NOT manage per-module settings.
|
|
13
|
+
|
|
14
|
+
Unix: $XDG_CONFIG_HOME/pk/pk.json (default ~/.config/pk/pk.json)
|
|
15
|
+
Windows: %APPDATA%/pk/pk.json
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
IS_WIN = sys.platform == "win32"
|
|
19
|
+
|
|
20
|
+
if IS_WIN:
|
|
21
|
+
CONFIG_DIR = Path(os.environ.get("APPDATA", "~/AppData/Roaming")) / "pk"
|
|
22
|
+
else:
|
|
23
|
+
CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "pk"
|
|
24
|
+
|
|
25
|
+
CONFIG_FILE = CONFIG_DIR / "pk.json"
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
self._data: dict = {}
|
|
30
|
+
if self.CONFIG_FILE.exists():
|
|
31
|
+
self._data = json.loads(self.CONFIG_FILE.read_text())
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def verbose(self) -> bool:
|
|
35
|
+
return self._data.get("verbose", False)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def profile(self) -> str:
|
|
39
|
+
return self._data.get("profile", "default")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def output(self) -> str:
|
|
43
|
+
return self._data.get("output", "table")
|
|
44
|
+
|
|
45
|
+
def set(self, key: str, value: object) -> None:
|
|
46
|
+
self._data[key] = value
|
|
47
|
+
self.CONFIG_FILE.write_text(json.dumps(self._data, indent=2, ensure_ascii=False))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_module_config_path(module_name: str) -> Path:
|
|
51
|
+
"""Return per-module config path: <CONFIG_DIR>/<module_name>.json."""
|
|
52
|
+
return PKConfig.CONFIG_DIR / f"{module_name}.json"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Unified terminal output via Rich Console with PK_THEME."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.theme import Theme
|
|
7
|
+
|
|
8
|
+
PK_THEME = Theme(
|
|
9
|
+
{
|
|
10
|
+
"info": "cyan",
|
|
11
|
+
"success": "green",
|
|
12
|
+
"warning": "yellow",
|
|
13
|
+
"error": "red bold",
|
|
14
|
+
"title": "bold magenta",
|
|
15
|
+
"key": "bold blue",
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
std_console = Console(theme=PK_THEME)
|
|
20
|
+
error_console = Console(stderr=True, theme=PK_THEME)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""PKError — base exception class for paykka-kit, all errors derive from here."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PKError(typer.Exit):
|
|
9
|
+
"""Base class for all paykka-kit exceptions.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
message: Human-readable error description.
|
|
13
|
+
code: Exit code, defaults to 1.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str = "", *, code: int = 1) -> None:
|
|
17
|
+
super().__init__(code=code)
|
|
18
|
+
self.message = message
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
return self.message
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Version lookup — reads from installed package metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_version() -> str:
|
|
9
|
+
"""Return the installed paykka-kit version string."""
|
|
10
|
+
try:
|
|
11
|
+
return version("paykka-kit")
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
return "dev"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""paykka-kit CLI entry point — 'pk' command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from paykka_kit.core.console import std_console
|
|
10
|
+
from paykka_kit.core.version import get_version
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="pk",
|
|
14
|
+
help="Unified CLI toolkit",
|
|
15
|
+
rich_markup_mode="rich",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _version_callback(*, value: bool) -> None:
|
|
21
|
+
"""Callback for --version / -V: print version and exit."""
|
|
22
|
+
if value:
|
|
23
|
+
std_console.print(f"pk [bold]{get_version()}[/bold]")
|
|
24
|
+
raise typer.Exit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.callback()
|
|
28
|
+
def main(
|
|
29
|
+
ctx: typer.Context,
|
|
30
|
+
verbose: Annotated[
|
|
31
|
+
bool,
|
|
32
|
+
typer.Option("--verbose", "-v", help="Enable verbose debug output"),
|
|
33
|
+
] = False,
|
|
34
|
+
output: Annotated[
|
|
35
|
+
str,
|
|
36
|
+
typer.Option("--output", "-o", help="Output format: json or table"),
|
|
37
|
+
] = "table",
|
|
38
|
+
version: Annotated[
|
|
39
|
+
bool | None,
|
|
40
|
+
typer.Option(
|
|
41
|
+
"--version",
|
|
42
|
+
"-V",
|
|
43
|
+
help="Show version and exit",
|
|
44
|
+
callback=_version_callback,
|
|
45
|
+
is_eager=True,
|
|
46
|
+
),
|
|
47
|
+
] = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Global options injected into context for all subcommands."""
|
|
50
|
+
ctx.ensure_object(dict)
|
|
51
|
+
ctx.obj["verbose"] = verbose
|
|
52
|
+
ctx.obj["output"] = output
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cli() -> None:
|
|
56
|
+
"""Programmatic entry point — used by pyproject.toml [project.scripts]."""
|
|
57
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""""""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""PKTool — abstract base class for all CLI tool modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PKTool(ABC):
|
|
11
|
+
"""Abstract base for every CLI tool under paykka-kit.
|
|
12
|
+
|
|
13
|
+
Each subclass MUST define:
|
|
14
|
+
- name: the subcommand name (e.g. "opsany")
|
|
15
|
+
- help: a one-line description for the help text
|
|
16
|
+
- register(): return a configured typer.Typer instance
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str = ""
|
|
20
|
+
help: str = ""
|
|
21
|
+
|
|
22
|
+
def __init_subclass__(cls, **kwargs: object) -> None:
|
|
23
|
+
super().__init_subclass__(**kwargs)
|
|
24
|
+
if cls.__name__ == "PKTool" or cls.__module__.endswith(".base"):
|
|
25
|
+
return # Allow the base class itself to be defined
|
|
26
|
+
if not cls.name:
|
|
27
|
+
raise TypeError(f"{cls.__name__} must define a 'name' class attribute")
|
|
28
|
+
if not cls.help:
|
|
29
|
+
raise TypeError(f"{cls.__name__} must define a 'help' class attribute")
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def register(self) -> typer.Typer:
|
|
33
|
+
"""Return a configured typer.Typer for this tool."""
|
|
34
|
+
...
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""""""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Unified output formatters — json / table."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
_PLACEHOLDER = "[dim]—[/dim]"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _cell(v: Any) -> str:
|
|
15
|
+
"""Format a table cell value, using dim placeholder for None / empty string."""
|
|
16
|
+
if v is None or v == "":
|
|
17
|
+
return _PLACEHOLDER
|
|
18
|
+
return str(v)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def render_output(data: list[dict[str, Any]], fmt: str, console: Console) -> None:
|
|
22
|
+
"""Render data in the specified format.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
data: List of dicts to render.
|
|
26
|
+
fmt: Output format, either "json" or "table".
|
|
27
|
+
console: Rich Console instance.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If the format is unsupported.
|
|
31
|
+
"""
|
|
32
|
+
if fmt == "json":
|
|
33
|
+
console.print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
34
|
+
elif fmt == "table":
|
|
35
|
+
if not data:
|
|
36
|
+
console.print("[dim](no data)[/dim]")
|
|
37
|
+
return
|
|
38
|
+
table = Table(show_edge=False, show_lines=False)
|
|
39
|
+
for key in data[0]:
|
|
40
|
+
table.add_column(key)
|
|
41
|
+
for row in data:
|
|
42
|
+
table.add_row(*[_cell(v) for v in row.values()])
|
|
43
|
+
console.print(table)
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError(f"Unsupported output format: {fmt}")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "paykka-kit"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "统一 CLI 工具集"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"typer>=0.21.0",
|
|
9
|
+
"rich>=14.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
dev = [
|
|
14
|
+
"pytest>=9.0",
|
|
15
|
+
"pytest-cov>=7.0",
|
|
16
|
+
"ruff>=0.12.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
paykka-kit = "paykka_kit.main:cli"
|
|
21
|
+
pk = "paykka_kit.main:cli"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.version]
|
|
24
|
+
path = "paykka_kit/_version.py"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling>=1.27"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Dynamic version generator: YY.M.D[.commit_short]"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from datetime import date
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_version() -> str:
|
|
11
|
+
"""Generate version string based on today's date and optional commit SHA."""
|
|
12
|
+
today = date.today()
|
|
13
|
+
base = f"{today.year % 100}.{today.month}.{today.day}"
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
sha = (
|
|
17
|
+
subprocess.check_output(
|
|
18
|
+
["git", "rev-parse", "--short=7", "HEAD"],
|
|
19
|
+
stderr=subprocess.DEVNULL,
|
|
20
|
+
)
|
|
21
|
+
.decode()
|
|
22
|
+
.strip()
|
|
23
|
+
)
|
|
24
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
25
|
+
sha = ""
|
|
26
|
+
|
|
27
|
+
# Check if HEAD already has a tag (first release of the day)
|
|
28
|
+
try:
|
|
29
|
+
existing_tags = (
|
|
30
|
+
subprocess.check_output(
|
|
31
|
+
["git", "tag", "--points-at", "HEAD"],
|
|
32
|
+
stderr=subprocess.DEVNULL,
|
|
33
|
+
)
|
|
34
|
+
.decode()
|
|
35
|
+
.strip()
|
|
36
|
+
)
|
|
37
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
38
|
+
existing_tags = ""
|
|
39
|
+
|
|
40
|
+
if existing_tags or not sha:
|
|
41
|
+
return base
|
|
42
|
+
return f"{base}.{sha}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> None:
|
|
46
|
+
version = get_version()
|
|
47
|
+
# Write version to _version.py so hatchling can pick it up
|
|
48
|
+
version_file = Path("paykka_kit") / "_version.py"
|
|
49
|
+
version_file.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
version_file.write_text(f'__version__ = "{version}"\n')
|
|
51
|
+
print(f"Version: {version}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""""""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""""""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Tests for PKConfig."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from paykka_kit.core.config import PKConfig, get_module_config_path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestPKConfig:
|
|
14
|
+
"""PKConfig global config tests."""
|
|
15
|
+
|
|
16
|
+
def test_config_dir_is_xdg_based(self) -> None:
|
|
17
|
+
"""On Unix, config directory follows XDG spec."""
|
|
18
|
+
config_dir = PKConfig.CONFIG_DIR
|
|
19
|
+
assert config_dir.name == "pk"
|
|
20
|
+
assert ".config" in str(config_dir)
|
|
21
|
+
|
|
22
|
+
def test_config_file_is_pk_json(self) -> None:
|
|
23
|
+
"""Config file is named pk.json."""
|
|
24
|
+
config_file = PKConfig.CONFIG_FILE
|
|
25
|
+
assert config_file.name == "pk.json"
|
|
26
|
+
|
|
27
|
+
def test_default_values(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
28
|
+
"""Returns defaults when no config file exists."""
|
|
29
|
+
monkeypatch.setattr(PKConfig, "CONFIG_FILE", tmp_path / "pk.json")
|
|
30
|
+
cfg = PKConfig()
|
|
31
|
+
assert cfg.verbose is False
|
|
32
|
+
assert cfg.profile == "default"
|
|
33
|
+
assert cfg.output == "table"
|
|
34
|
+
|
|
35
|
+
def test_load_from_file(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
36
|
+
"""Loads values from an existing config file."""
|
|
37
|
+
config_file = tmp_path / "pk.json"
|
|
38
|
+
config_file.write_text(json.dumps({"verbose": True, "profile": "staging"}))
|
|
39
|
+
monkeypatch.setattr(PKConfig, "CONFIG_FILE", config_file)
|
|
40
|
+
cfg = PKConfig()
|
|
41
|
+
assert cfg.verbose is True
|
|
42
|
+
assert cfg.profile == "staging"
|
|
43
|
+
|
|
44
|
+
def test_set_saves_to_file(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
45
|
+
"""set() persists values to the config file."""
|
|
46
|
+
config_file = tmp_path / "pk.json"
|
|
47
|
+
monkeypatch.setattr(PKConfig, "CONFIG_FILE", config_file)
|
|
48
|
+
cfg = PKConfig()
|
|
49
|
+
cfg.set("verbose", True)
|
|
50
|
+
cfg.set("output", "json")
|
|
51
|
+
assert config_file.exists()
|
|
52
|
+
data = json.loads(config_file.read_text())
|
|
53
|
+
assert data["verbose"] is True
|
|
54
|
+
assert data["output"] == "json"
|
|
55
|
+
|
|
56
|
+
def test_empty_config_file(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
57
|
+
"""Empty config file uses defaults."""
|
|
58
|
+
config_file = tmp_path / "pk.json"
|
|
59
|
+
config_file.write_text(json.dumps({}))
|
|
60
|
+
monkeypatch.setattr(PKConfig, "CONFIG_FILE", config_file)
|
|
61
|
+
cfg = PKConfig()
|
|
62
|
+
assert cfg.verbose is False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestGetModuleConfigPath:
|
|
66
|
+
"""get_module_config_path tests."""
|
|
67
|
+
|
|
68
|
+
def test_returns_config_dir_path(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
69
|
+
"""Returns <CONFIG_DIR>/<module_name>.json."""
|
|
70
|
+
monkeypatch.setattr(PKConfig, "CONFIG_DIR", Path("/tmp/pk"))
|
|
71
|
+
path = get_module_config_path("opsany")
|
|
72
|
+
assert path == Path("/tmp/pk/opsany.json")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestPKDir:
|
|
76
|
+
"""Config directory creation tests."""
|
|
77
|
+
|
|
78
|
+
def test_config_dir_created_on_init(
|
|
79
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Config directory is auto-created on first init."""
|
|
82
|
+
pk_dir = tmp_path / "pk"
|
|
83
|
+
monkeypatch.setattr(PKConfig, "CONFIG_DIR", pk_dir)
|
|
84
|
+
monkeypatch.setattr(PKConfig, "CONFIG_FILE", pk_dir / "pk.json")
|
|
85
|
+
PKConfig()
|
|
86
|
+
assert pk_dir.exists()
|
|
87
|
+
assert pk_dir.is_dir()
|