opencode-db 0.3.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.
- cli.py +70 -0
- cmd_costs.py +197 -0
- cmd_delete.py +58 -0
- cmd_export.py +189 -0
- cmd_help.py +210 -0
- cmd_info.py +152 -0
- cmd_list.py +124 -0
- cmd_projects.py +82 -0
- cmd_prune.py +127 -0
- cmd_search.py +111 -0
- cmd_stats.py +115 -0
- cmd_todos.py +90 -0
- cmd_tree.py +95 -0
- cmd_vacuum.py +67 -0
- commands/__init__.py +51 -0
- config.py +14 -0
- db.py +374 -0
- formatters.py +208 -0
- i18n.py +532 -0
- opencode_db-0.3.0.dist-info/METADATA +7 -0
- opencode_db-0.3.0.dist-info/RECORD +26 -0
- opencode_db-0.3.0.dist-info/WHEEL +5 -0
- opencode_db-0.3.0.dist-info/entry_points.txt +2 -0
- opencode_db-0.3.0.dist-info/licenses/LICENSE +25 -0
- opencode_db-0.3.0.dist-info/top_level.txt +20 -0
- utils.py +115 -0
utils.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Вспомогательные утилиты.
|
|
2
|
+
|
|
3
|
+
Функции общего назначения:
|
|
4
|
+
- parse_ts() / format_ts() — работа с миллисекундными таймштампами
|
|
5
|
+
- summarize_val() — обрезка длинных строк
|
|
6
|
+
- parse_time_spec() — парсинг '30d', '6m', '1y'
|
|
7
|
+
- confirm() — запрос подтверждения
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from datetime import UTC, datetime, timedelta
|
|
12
|
+
from typing import Literal, LiteralString
|
|
13
|
+
|
|
14
|
+
from i18n import _
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_ts(ts_ms) -> datetime:
|
|
18
|
+
"""Конвертирует Unix timestamp в миллисекундах в datetime (UTC)."""
|
|
19
|
+
return datetime.fromtimestamp(ts_ms / 1000, tz=UTC)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def format_ts(ts_ms, fmt="%Y-%m-%d %H:%M:%S") -> str:
|
|
23
|
+
"""Форматирует timestamp в строку."""
|
|
24
|
+
return parse_ts(ts_ms).strftime(fmt)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def format_ts_short(ts_ms) -> str:
|
|
28
|
+
"""Форматирует timestamp в компактный формат для имён файлов."""
|
|
29
|
+
return format_ts(ts_ms, "%Y-%m-%d_%H.%M.%S")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def summarize_val(v, max_len=120) -> str:
|
|
33
|
+
"""Обрезает длинное значение для компактного отображения."""
|
|
34
|
+
s = str(v)
|
|
35
|
+
if len(s) > max_len:
|
|
36
|
+
s = s[:max_len] + "..."
|
|
37
|
+
return s
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_time_spec(spec) -> None | datetime:
|
|
41
|
+
"""Парсит спецификацию времени ('30d', '6m', '1y') в timedelta."""
|
|
42
|
+
if not spec or not isinstance(spec, str):
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
spec = spec.strip().lower()
|
|
46
|
+
match = re.match(r"^(\d+)\s*(d|day|days|m|mo|mon|month|months|y|year|years)$", spec)
|
|
47
|
+
if not match:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
value = int(match.group(1))
|
|
51
|
+
unit = match.group(2)
|
|
52
|
+
|
|
53
|
+
now = datetime.now(UTC)
|
|
54
|
+
|
|
55
|
+
if unit == "d" or unit.startswith("day"):
|
|
56
|
+
delta = timedelta(days=value)
|
|
57
|
+
elif unit == "m" or unit.startswith("mon"):
|
|
58
|
+
delta = timedelta(days=value * 30.44)
|
|
59
|
+
elif unit == "y" or unit.startswith("year"):
|
|
60
|
+
delta = timedelta(days=value * 365.25)
|
|
61
|
+
else:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
return now - delta
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def confirm(prompt=None, default=False) -> bool:
|
|
68
|
+
"""Запрашивает подтверждение y/N.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
prompt: str, текст приглашения (если None — используется общий)
|
|
72
|
+
default: bool, что возвращать при пустом вводе
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
bool
|
|
76
|
+
"""
|
|
77
|
+
if prompt is None:
|
|
78
|
+
prompt = _("yes_no_prompt")
|
|
79
|
+
suffix = _("yes_no_true") if default else _("yes_no_false")
|
|
80
|
+
while True:
|
|
81
|
+
try:
|
|
82
|
+
answer = input(prompt + suffix).strip().lower()
|
|
83
|
+
except (EOFError, KeyboardInterrupt):
|
|
84
|
+
print()
|
|
85
|
+
return False
|
|
86
|
+
if not answer:
|
|
87
|
+
return default
|
|
88
|
+
if answer in ("y", "yes", _("cli.yn_yes")):
|
|
89
|
+
return True
|
|
90
|
+
if answer in ("n", "no", "нет"):
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def format_cost(cost_value) -> str:
|
|
95
|
+
"""Форматирует стоимость в долларах."""
|
|
96
|
+
if cost_value is None:
|
|
97
|
+
return "—"
|
|
98
|
+
if cost_value < 0.01:
|
|
99
|
+
return f"${cost_value:.4f}"
|
|
100
|
+
if cost_value < 1:
|
|
101
|
+
return f"${cost_value:.3f}"
|
|
102
|
+
return f"${cost_value:.2f}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def format_tokens(count) -> LiteralString | Literal["—"]:
|
|
106
|
+
"""Форматирует количество токенов с разделителями."""
|
|
107
|
+
if count is None:
|
|
108
|
+
return "—"
|
|
109
|
+
s = str(int(count))
|
|
110
|
+
result = []
|
|
111
|
+
for i, ch in enumerate(reversed(s)):
|
|
112
|
+
if i > 0 and i % 3 == 0:
|
|
113
|
+
result.append("\u202f")
|
|
114
|
+
result.append(ch)
|
|
115
|
+
return "".join(reversed(result))
|