day-recap 0.2.1__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.
- day_recap-0.2.1/PKG-INFO +116 -0
- day_recap-0.2.1/README.md +103 -0
- day_recap-0.2.1/app/__init__.py +0 -0
- day_recap-0.2.1/app/atomic.py +36 -0
- day_recap-0.2.1/app/cli.py +350 -0
- day_recap-0.2.1/app/collectors/__init__.py +1 -0
- day_recap-0.2.1/app/collectors/git.py +447 -0
- day_recap-0.2.1/app/collectors/opencode.py +184 -0
- day_recap-0.2.1/app/collectors/replies.py +69 -0
- day_recap-0.2.1/app/collectors/telegram/__init__.py +1 -0
- day_recap-0.2.1/app/collectors/telegram/chats.py +70 -0
- day_recap-0.2.1/app/collectors/telegram/collector.py +505 -0
- day_recap-0.2.1/app/collectors/telegram/init.py +409 -0
- day_recap-0.2.1/app/collectors/telegram/messages.py +296 -0
- day_recap-0.2.1/app/collectors/telegram/tg.py +235 -0
- day_recap-0.2.1/app/collectors/transcripts.py +167 -0
- day_recap-0.2.1/app/config.py +376 -0
- day_recap-0.2.1/app/errors.py +40 -0
- day_recap-0.2.1/app/notes/__init__.py +1 -0
- day_recap-0.2.1/app/notes/writer.py +175 -0
- day_recap-0.2.1/app/py.typed +0 -0
- day_recap-0.2.1/app/summary/__init__.py +0 -0
- day_recap-0.2.1/app/summary/collect.py +423 -0
- day_recap-0.2.1/app/summary/masking.py +87 -0
- day_recap-0.2.1/app/summary/models.py +169 -0
- day_recap-0.2.1/app/summary/show.py +181 -0
- day_recap-0.2.1/pyproject.toml +100 -0
- day_recap-0.2.1/pyproject.toml.orig +99 -0
day_recap-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: day-recap
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Саммари проделанной работы за день: задачи и принятые решения из git, транскриптов агентов и чатов Telegram
|
|
5
|
+
Author: Vladimir Voronov
|
|
6
|
+
Author-email: Vladimir Voronov <vvoronov@mwnts.ru>
|
|
7
|
+
Requires-Dist: pydantic>=2.13.5
|
|
8
|
+
Requires-Dist: pydantic-settings>=2.15.0
|
|
9
|
+
Requires-Dist: qrcode>=8.2
|
|
10
|
+
Requires-Dist: telethon>=1.45.0
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# day-recap
|
|
15
|
+
|
|
16
|
+
[Русский](README.ru.md)
|
|
17
|
+
|
|
18
|
+
[](https://github.com/redb0/day-recap/actions/workflows/test.yaml)
|
|
19
|
+
[](https://codecov.io/gh/redb0/day-recap)
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
Collects a day's work from local git repositories, Cursor transcripts, OpenCode
|
|
23
|
+
sessions, and a Telegram allowlist. Only a confirmed block is appended to the
|
|
24
|
+
Obsidian daily note.
|
|
25
|
+
|
|
26
|
+
The raw dump stays outside the vault. An empty day does not change the note.
|
|
27
|
+
|
|
28
|
+
```markdown
|
|
29
|
+
## 🤖 Итоги дня
|
|
30
|
+
|
|
31
|
+
### Сделано
|
|
32
|
+
- **billing** — сузил окно повторной оплаты до одного счёта
|
|
33
|
+
|
|
34
|
+
### Решения и обоснования
|
|
35
|
+
Оставили повтор по idempotency-key. Повтор всей корзины отбросили: он списывал деньги второй раз.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Install and configure
|
|
39
|
+
|
|
40
|
+
From a checkout of this repository:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
uv tool install --editable .
|
|
44
|
+
mkdir -p ~/.config/day-recap
|
|
45
|
+
cp config.example.toml ~/.config/day-recap/config.toml
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Run `day-recap init` in your own terminal, not in the agent chat. Do not
|
|
49
|
+
type `api_id` or `api_hash` into the chat. `init` asks `qr` or `code`. `qr`
|
|
50
|
+
prints a code to scan: Settings → Devices → Link desktop device. `code` waits
|
|
51
|
+
for the login code in the Telegram service chat. `--login qr` or `--login code`
|
|
52
|
+
skips the question. `--relogin` signs in again.
|
|
53
|
+
|
|
54
|
+
`day-recap chats` prints a table and ready `[[telegram.chats]]` blocks.
|
|
55
|
+
Paste the blocks you want into the `[telegram]` section. Only those chats are
|
|
56
|
+
read into the raw dump.
|
|
57
|
+
|
|
58
|
+
| Section | What it sets |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `[notes]` | Where to write the daily note. |
|
|
61
|
+
| `[state]` | Where raw dumps are stored. |
|
|
62
|
+
| `[git]` | Which directories to walk and whose commits to take. |
|
|
63
|
+
| `[transcripts]` | Cursor JSONL roots and how messages are trimmed. |
|
|
64
|
+
| `[opencode]` | OpenCode database and the same trim. |
|
|
65
|
+
| `[telegram]` | Whether the source is on, and the allowlist. |
|
|
66
|
+
| `[summary]` | Size threshold. |
|
|
67
|
+
|
|
68
|
+
Keys and defaults are in [`config.example.toml`](config.example.toml).
|
|
69
|
+
|
|
70
|
+
- The size threshold is 100000 bytes.
|
|
71
|
+
- Raw dumps are kept for 14 days.
|
|
72
|
+
- `~/.config/day-recap/.env` is mode 600.
|
|
73
|
+
|
|
74
|
+
## In the agent
|
|
75
|
+
|
|
76
|
+
Copy [`SKILL.md`](SKILL.md) to `~/.agents/skills/day-recap/SKILL.md`.
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
/day-recap
|
|
80
|
+
/day-recap yesterday
|
|
81
|
+
/day-recap 2026-10-05
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The date is `today`, `yesterday`, or `YYYY-MM-DD`. With no date, the window
|
|
85
|
+
runs from local midnight to the moment you start.
|
|
86
|
+
|
|
87
|
+
The skill shows a draft and asks what you decided and what you rejected. The
|
|
88
|
+
note changes only after a separate yes. An empty day does not change the note.
|
|
89
|
+
With no Telegram session the other sources are still collected, and the block
|
|
90
|
+
contains the line «Telegram недоступен, чаты не учтены».
|
|
91
|
+
|
|
92
|
+
## Other commands
|
|
93
|
+
|
|
94
|
+
`collect` stores a raw dump. `show` prints the day status, or one source when
|
|
95
|
+
you name it. The source is optional: `git`, `transcripts`, `opencode`, or
|
|
96
|
+
`telegram`. Both take `--date` (`today`, `yesterday`, or `YYYY-MM-DD`) and
|
|
97
|
+
`--config PATH`.
|
|
98
|
+
|
|
99
|
+
`write` reads the block from stdin. It needs `--date YYYY-MM-DD` and
|
|
100
|
+
`--body -`, and takes `--config PATH`. Without `--apply` the note stays
|
|
101
|
+
unchanged.
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
day-recap collect --date yesterday
|
|
105
|
+
day-recap show git --date 2026-10-05
|
|
106
|
+
day-recap write --date 2026-10-05 --body - --apply
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
just lint # ruff
|
|
113
|
+
just fmt # ruff format
|
|
114
|
+
just types # mypy
|
|
115
|
+
just test # pytest
|
|
116
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# day-recap
|
|
2
|
+
|
|
3
|
+
[Русский](README.ru.md)
|
|
4
|
+
|
|
5
|
+
[](https://github.com/redb0/day-recap/actions/workflows/test.yaml)
|
|
6
|
+
[](https://codecov.io/gh/redb0/day-recap)
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
Collects a day's work from local git repositories, Cursor transcripts, OpenCode
|
|
10
|
+
sessions, and a Telegram allowlist. Only a confirmed block is appended to the
|
|
11
|
+
Obsidian daily note.
|
|
12
|
+
|
|
13
|
+
The raw dump stays outside the vault. An empty day does not change the note.
|
|
14
|
+
|
|
15
|
+
```markdown
|
|
16
|
+
## 🤖 Итоги дня
|
|
17
|
+
|
|
18
|
+
### Сделано
|
|
19
|
+
- **billing** — сузил окно повторной оплаты до одного счёта
|
|
20
|
+
|
|
21
|
+
### Решения и обоснования
|
|
22
|
+
Оставили повтор по idempotency-key. Повтор всей корзины отбросили: он списывал деньги второй раз.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Install and configure
|
|
26
|
+
|
|
27
|
+
From a checkout of this repository:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
uv tool install --editable .
|
|
31
|
+
mkdir -p ~/.config/day-recap
|
|
32
|
+
cp config.example.toml ~/.config/day-recap/config.toml
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run `day-recap init` in your own terminal, not in the agent chat. Do not
|
|
36
|
+
type `api_id` or `api_hash` into the chat. `init` asks `qr` or `code`. `qr`
|
|
37
|
+
prints a code to scan: Settings → Devices → Link desktop device. `code` waits
|
|
38
|
+
for the login code in the Telegram service chat. `--login qr` or `--login code`
|
|
39
|
+
skips the question. `--relogin` signs in again.
|
|
40
|
+
|
|
41
|
+
`day-recap chats` prints a table and ready `[[telegram.chats]]` blocks.
|
|
42
|
+
Paste the blocks you want into the `[telegram]` section. Only those chats are
|
|
43
|
+
read into the raw dump.
|
|
44
|
+
|
|
45
|
+
| Section | What it sets |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `[notes]` | Where to write the daily note. |
|
|
48
|
+
| `[state]` | Where raw dumps are stored. |
|
|
49
|
+
| `[git]` | Which directories to walk and whose commits to take. |
|
|
50
|
+
| `[transcripts]` | Cursor JSONL roots and how messages are trimmed. |
|
|
51
|
+
| `[opencode]` | OpenCode database and the same trim. |
|
|
52
|
+
| `[telegram]` | Whether the source is on, and the allowlist. |
|
|
53
|
+
| `[summary]` | Size threshold. |
|
|
54
|
+
|
|
55
|
+
Keys and defaults are in [`config.example.toml`](config.example.toml).
|
|
56
|
+
|
|
57
|
+
- The size threshold is 100000 bytes.
|
|
58
|
+
- Raw dumps are kept for 14 days.
|
|
59
|
+
- `~/.config/day-recap/.env` is mode 600.
|
|
60
|
+
|
|
61
|
+
## In the agent
|
|
62
|
+
|
|
63
|
+
Copy [`SKILL.md`](SKILL.md) to `~/.agents/skills/day-recap/SKILL.md`.
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
/day-recap
|
|
67
|
+
/day-recap yesterday
|
|
68
|
+
/day-recap 2026-10-05
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The date is `today`, `yesterday`, or `YYYY-MM-DD`. With no date, the window
|
|
72
|
+
runs from local midnight to the moment you start.
|
|
73
|
+
|
|
74
|
+
The skill shows a draft and asks what you decided and what you rejected. The
|
|
75
|
+
note changes only after a separate yes. An empty day does not change the note.
|
|
76
|
+
With no Telegram session the other sources are still collected, and the block
|
|
77
|
+
contains the line «Telegram недоступен, чаты не учтены».
|
|
78
|
+
|
|
79
|
+
## Other commands
|
|
80
|
+
|
|
81
|
+
`collect` stores a raw dump. `show` prints the day status, or one source when
|
|
82
|
+
you name it. The source is optional: `git`, `transcripts`, `opencode`, or
|
|
83
|
+
`telegram`. Both take `--date` (`today`, `yesterday`, or `YYYY-MM-DD`) and
|
|
84
|
+
`--config PATH`.
|
|
85
|
+
|
|
86
|
+
`write` reads the block from stdin. It needs `--date YYYY-MM-DD` and
|
|
87
|
+
`--body -`, and takes `--config PATH`. Without `--apply` the note stays
|
|
88
|
+
unchanged.
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
day-recap collect --date yesterday
|
|
92
|
+
day-recap show git --date 2026-10-05
|
|
93
|
+
day-recap write --date 2026-10-05 --body - --apply
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
just lint # ruff
|
|
100
|
+
just fmt # ruff format
|
|
101
|
+
just types # mypy
|
|
102
|
+
just test # pytest
|
|
103
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Замена файла целиком: обрыв не оставляет обрезанный текст."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def replace_text(path: Path, text: str, *, mode: int | None = None) -> None:
|
|
9
|
+
"""Записать текст через временный файл и `os.replace`.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
path: Куда положить готовый текст.
|
|
13
|
+
text: Содержимое.
|
|
14
|
+
mode: Права нового файла. `None` — оставить режим существующего
|
|
15
|
+
или `0o644`, если файла ещё нет.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
OSError: Каталог недоступен или замена не удалась.
|
|
19
|
+
"""
|
|
20
|
+
chosen = _mode(path) if mode is None else mode
|
|
21
|
+
fd, name = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
|
|
22
|
+
tmp = Path(name)
|
|
23
|
+
try:
|
|
24
|
+
os.fchmod(fd, chosen)
|
|
25
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="") as handle:
|
|
26
|
+
handle.write(text)
|
|
27
|
+
tmp.replace(path)
|
|
28
|
+
except OSError:
|
|
29
|
+
tmp.unlink(missing_ok=True)
|
|
30
|
+
raise
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _mode(path: Path) -> int:
|
|
34
|
+
if path.is_file():
|
|
35
|
+
return path.stat().st_mode & 0o777
|
|
36
|
+
return 0o644
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Точка входа команды `day-recap`."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import getpass
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from datetime import date
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from app.collectors.telegram.chats import load_chats
|
|
13
|
+
from app.collectors.telegram.init import run_init
|
|
14
|
+
from app.config import Config, load_config
|
|
15
|
+
from app.errors import ErrorCode, SummaryError
|
|
16
|
+
from app.notes.writer import write_note
|
|
17
|
+
from app.summary.collect import (
|
|
18
|
+
RELATIVE_DAYS,
|
|
19
|
+
SOURCES,
|
|
20
|
+
CollectedDay,
|
|
21
|
+
SourceName,
|
|
22
|
+
collect_and_store,
|
|
23
|
+
load_stored_day,
|
|
24
|
+
local_now,
|
|
25
|
+
resolve_day,
|
|
26
|
+
)
|
|
27
|
+
from app.summary.models import Dump, GitDump, SessionDump, SourceStatus, TelegramDump, Window
|
|
28
|
+
from app.summary.show import render_git, render_sessions, render_telegram, visible_truncation
|
|
29
|
+
|
|
30
|
+
DISTRIBUTION = "day-recap"
|
|
31
|
+
_NOT_KEPT = "дамп не сохранён: дата старше ретенции"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
35
|
+
"""Собрать разборщик аргументов.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Разборщик верхнего уровня.
|
|
39
|
+
"""
|
|
40
|
+
parser = argparse.ArgumentParser(
|
|
41
|
+
prog=DISTRIBUTION,
|
|
42
|
+
description="Саммари проделанной за день работы: задачи и принятые решения.",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--version",
|
|
46
|
+
action="version",
|
|
47
|
+
version=f"%(prog)s {version(DISTRIBUTION)}",
|
|
48
|
+
)
|
|
49
|
+
commands = parser.add_subparsers(dest="command", required=False)
|
|
50
|
+
collect = commands.add_parser("collect", help="Собрать сырой дамп за день.")
|
|
51
|
+
collect.add_argument("source", nargs="?", choices=SOURCES, default=None)
|
|
52
|
+
collect.add_argument("--date", type=_day_token, default=None)
|
|
53
|
+
collect.add_argument("--config", type=Path, default=None)
|
|
54
|
+
show = commands.add_parser("show", help="Показать сводку или тело источника.")
|
|
55
|
+
show.add_argument("source", nargs="?", choices=SOURCES, default=None)
|
|
56
|
+
show.add_argument("--date", type=_day_token, default=None)
|
|
57
|
+
show.add_argument("--config", type=Path, default=None)
|
|
58
|
+
write = commands.add_parser("write", help="Записать блок итогов в ежедневную заметку.")
|
|
59
|
+
write.add_argument("--date", type=_iso_day, required=True)
|
|
60
|
+
write.add_argument("--body", type=_stdin_body, required=True)
|
|
61
|
+
write.add_argument("--apply", action="store_true")
|
|
62
|
+
write.add_argument("--config", type=Path, default=None)
|
|
63
|
+
init = commands.add_parser("init", help="Настроить доступ к Telegram в своём терминале.")
|
|
64
|
+
init.add_argument("--relogin", action="store_true")
|
|
65
|
+
init.add_argument("--login", choices=("qr", "code"), default=None)
|
|
66
|
+
init.add_argument("--config", type=Path, default=None)
|
|
67
|
+
chats = commands.add_parser("chats", help="Показать чаты и фрагмент конфига.")
|
|
68
|
+
chats.add_argument("--config", type=Path, default=None)
|
|
69
|
+
return parser
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
73
|
+
"""Разобрать аргументы и выполнить команду.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
argv: Аргументы без имени программы. `None` — взять из `sys.argv`.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Код возврата процесса.
|
|
80
|
+
"""
|
|
81
|
+
parser = build_parser()
|
|
82
|
+
args = parser.parse_args(argv)
|
|
83
|
+
if args.command is None:
|
|
84
|
+
parser.print_help()
|
|
85
|
+
return 0
|
|
86
|
+
try:
|
|
87
|
+
return _dispatch(args)
|
|
88
|
+
except SummaryError as error:
|
|
89
|
+
_print_error(error)
|
|
90
|
+
return 1
|
|
91
|
+
except FileNotFoundError as exc:
|
|
92
|
+
sys.stderr.write(f"Файл конфигурации не найден: {exc}\n")
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _day_token(value: str) -> str:
|
|
97
|
+
if value in RELATIVE_DAYS or _is_iso_day(value):
|
|
98
|
+
return value
|
|
99
|
+
message = "ожидается YYYY-MM-DD, today или yesterday"
|
|
100
|
+
raise argparse.ArgumentTypeError(message)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _iso_day(value: str) -> str:
|
|
104
|
+
if _is_iso_day(value):
|
|
105
|
+
return value
|
|
106
|
+
message = "ожидается YYYY-MM-DD"
|
|
107
|
+
raise argparse.ArgumentTypeError(message)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _is_iso_day(value: str) -> bool:
|
|
111
|
+
try:
|
|
112
|
+
date.fromisoformat(value)
|
|
113
|
+
except ValueError:
|
|
114
|
+
return False
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _stdin_body(value: str) -> str:
|
|
119
|
+
if value != "-":
|
|
120
|
+
message = "тело читается из stdin: укажите --body -"
|
|
121
|
+
raise argparse.ArgumentTypeError(message) from None
|
|
122
|
+
return value
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _dispatch(args: argparse.Namespace) -> int:
|
|
126
|
+
config = load_config(args.config)
|
|
127
|
+
if args.command == "collect":
|
|
128
|
+
return _run_collect(config, args.date, args.source)
|
|
129
|
+
if args.command == "show":
|
|
130
|
+
return _run_show(config, args.date, args.source)
|
|
131
|
+
if args.command == "write":
|
|
132
|
+
return _run_write(config, args.date, apply=args.apply)
|
|
133
|
+
if args.command == "init":
|
|
134
|
+
return _run_init(config, relogin=args.relogin, login=args.login)
|
|
135
|
+
return _run_chats(config)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _run_collect(config: Config, token: str | None, source: SourceName | None) -> int:
|
|
139
|
+
day = collect_and_store(config, token, progress=_progress, source=source)
|
|
140
|
+
sys.stdout.write(_report(day, threshold=config.summary.two_stage_threshold_bytes))
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _run_show(config: Config, token: str | None, source: SourceName | None) -> int:
|
|
145
|
+
day_date, stored = load_stored_day(config, token)
|
|
146
|
+
if source is None:
|
|
147
|
+
sys.stdout.write(
|
|
148
|
+
_report(
|
|
149
|
+
stored,
|
|
150
|
+
threshold=config.summary.two_stage_threshold_bytes,
|
|
151
|
+
empty_date=day_date,
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
return 0
|
|
155
|
+
sys.stdout.write(_source_text(source, stored))
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _source_text(source: SourceName, day: CollectedDay) -> str:
|
|
160
|
+
dump = _dumps(day)[source]
|
|
161
|
+
if dump is None:
|
|
162
|
+
return _source_line(source, None) + "\n"
|
|
163
|
+
return _body(dump)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _body(dump: Dump) -> str:
|
|
167
|
+
if isinstance(dump, GitDump):
|
|
168
|
+
return render_git(dump)
|
|
169
|
+
if isinstance(dump, TelegramDump):
|
|
170
|
+
return render_telegram(dump)
|
|
171
|
+
return render_sessions(dump)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _progress(line: str) -> None:
|
|
175
|
+
sys.stderr.write(f"{line}\n")
|
|
176
|
+
sys.stderr.flush()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _run_write(config: Config, token: str, *, apply: bool) -> int:
|
|
180
|
+
day = resolve_day(token, today=local_now(config.notes.timezone).date())
|
|
181
|
+
result = write_note(config, day, sys.stdin.read(), apply=apply)
|
|
182
|
+
sys.stdout.write(result.diff)
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _run_init(config: Config, *, relogin: bool, login: str | None) -> int:
|
|
187
|
+
account = run_init(
|
|
188
|
+
config,
|
|
189
|
+
relogin=relogin,
|
|
190
|
+
login=login,
|
|
191
|
+
ask=_ask_credential,
|
|
192
|
+
choose=_ask_login,
|
|
193
|
+
)
|
|
194
|
+
sys.stdout.write(f"Вошли как {account}\n")
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _ask_login() -> str:
|
|
199
|
+
while True:
|
|
200
|
+
answer = input("Вход (qr/code): ").strip().lower()
|
|
201
|
+
if answer in {"qr", "code"}:
|
|
202
|
+
return answer
|
|
203
|
+
sys.stderr.write("Ожидается qr или code.\n")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _ask_credential(key: str) -> str:
|
|
207
|
+
if key == "TG_API_HASH":
|
|
208
|
+
return getpass.getpass("TG_API_HASH: ")
|
|
209
|
+
if key == "TG_API_ID":
|
|
210
|
+
return input("TG_API_ID (https://my.telegram.org/apps): ")
|
|
211
|
+
return input("TG_PHONE (+7999…): ")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _run_chats(config: Config) -> int:
|
|
215
|
+
sys.stdout.write(asyncio.run(load_chats(config)))
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
_STATUS = {
|
|
220
|
+
SourceStatus.OK: "ok",
|
|
221
|
+
SourceStatus.EMPTY: "пустой",
|
|
222
|
+
SourceStatus.DISABLED: "выключен",
|
|
223
|
+
SourceStatus.UNAVAILABLE: "недоступен",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _report(
|
|
228
|
+
day: CollectedDay,
|
|
229
|
+
*,
|
|
230
|
+
threshold: int,
|
|
231
|
+
empty_date: date | None = None,
|
|
232
|
+
) -> str:
|
|
233
|
+
shown = _day_date(day, empty_date)
|
|
234
|
+
shared = _shared_window(day)
|
|
235
|
+
lines = [
|
|
236
|
+
*_heading(day),
|
|
237
|
+
f"дата: {shown.isoformat()}",
|
|
238
|
+
f"байты: {day.total_bytes}",
|
|
239
|
+
f"порог: {threshold}",
|
|
240
|
+
f"порог превышен: {_exceeded(day.total_bytes, threshold)}",
|
|
241
|
+
]
|
|
242
|
+
if shared is not None:
|
|
243
|
+
lines.append(_window_text(shared))
|
|
244
|
+
lines.extend(
|
|
245
|
+
_source_line(name, dump) + _window_suffix(dump, shared=shared is not None)
|
|
246
|
+
for name, dump in _dumps(day).items()
|
|
247
|
+
)
|
|
248
|
+
lines.extend(_notes(day))
|
|
249
|
+
return "\n".join(lines) + "\n"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _dumps(day: CollectedDay) -> dict[SourceName, Dump | None]:
|
|
253
|
+
return {
|
|
254
|
+
"git": day.git,
|
|
255
|
+
"transcripts": day.transcripts,
|
|
256
|
+
"opencode": day.opencode,
|
|
257
|
+
"telegram": day.telegram,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _shared_window(day: CollectedDay) -> Window | None:
|
|
262
|
+
windows = [dump.window for dump in _dumps(day).values() if dump is not None]
|
|
263
|
+
if not windows or any(item != windows[0] for item in windows):
|
|
264
|
+
return None
|
|
265
|
+
return windows[0]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _window_text(window: Window) -> str:
|
|
269
|
+
return f"окно: {window.from_.isoformat()}..{window.to.isoformat()}"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _window_suffix(dump: Dump | None, *, shared: bool) -> str:
|
|
273
|
+
if shared or dump is None:
|
|
274
|
+
return ""
|
|
275
|
+
return ", " + _window_text(dump.window)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _heading(day: CollectedDay) -> list[str]:
|
|
279
|
+
if day.directory is not None:
|
|
280
|
+
return [str(day.directory)]
|
|
281
|
+
if any(dump is not None for dump in _dumps(day).values()):
|
|
282
|
+
return [_NOT_KEPT]
|
|
283
|
+
return []
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _day_date(day: CollectedDay, fallback: date | None) -> date:
|
|
287
|
+
for dump in _dumps(day).values():
|
|
288
|
+
if dump is not None:
|
|
289
|
+
return dump.date
|
|
290
|
+
if fallback is not None:
|
|
291
|
+
return fallback
|
|
292
|
+
message = "в дне нет ни одного дампа"
|
|
293
|
+
raise RuntimeError(message)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _exceeded(total: int, threshold: int) -> str:
|
|
297
|
+
if total > threshold:
|
|
298
|
+
return "да"
|
|
299
|
+
return "нет"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _source_line(name: SourceName, dump: Dump | None) -> str:
|
|
303
|
+
if name == "git":
|
|
304
|
+
return _git_line(dump if isinstance(dump, GitDump) else None)
|
|
305
|
+
if name == "telegram":
|
|
306
|
+
return _telegram_line(dump if isinstance(dump, TelegramDump) else None)
|
|
307
|
+
return _session_line(name, dump if isinstance(dump, SessionDump) else None)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _login_code(dump: GitDump | SessionDump | TelegramDump) -> str:
|
|
311
|
+
if dump.code in {ErrorCode.NO_SESSION, ErrorCode.NO_CREDENTIALS}:
|
|
312
|
+
return f", code: {dump.code}"
|
|
313
|
+
return ""
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _git_line(dump: GitDump | None) -> str:
|
|
317
|
+
if dump is None:
|
|
318
|
+
return "git: не собран, репозиториев: 0, коммитов: 0"
|
|
319
|
+
commits = sum(len(repo.commits) for repo in dump.repos)
|
|
320
|
+
return (
|
|
321
|
+
f"git: {_STATUS[dump.status]}{_login_code(dump)}, "
|
|
322
|
+
f"репозиториев: {len(dump.repos)}, коммитов: {commits}"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _session_line(name: str, dump: SessionDump | None) -> str:
|
|
327
|
+
if dump is None:
|
|
328
|
+
return f"{name}: не собран, сессий: 0"
|
|
329
|
+
return f"{name}: {_STATUS[dump.status]}{_login_code(dump)}, сессий: {len(dump.sessions)}"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _telegram_line(dump: TelegramDump | None) -> str:
|
|
333
|
+
if dump is None:
|
|
334
|
+
return "telegram: не собран, чатов: 0, unlisted: 0"
|
|
335
|
+
return (
|
|
336
|
+
f"telegram: {_STATUS[dump.status]}{_login_code(dump)}, "
|
|
337
|
+
f"чатов: {len(dump.chats)}, unlisted: {dump.unlisted_active}"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _notes(day: CollectedDay) -> list[str]:
|
|
342
|
+
notes: list[str] = []
|
|
343
|
+
for dump in _dumps(day).values():
|
|
344
|
+
if dump is not None:
|
|
345
|
+
notes.extend(visible_truncation(note) for note in dump.truncations)
|
|
346
|
+
return notes
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _print_error(error: SummaryError) -> None:
|
|
350
|
+
sys.stderr.write(f"code: {error.code}\n{error.message}\n{error.hint}\n")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Сборщики источников day-recap."""
|