vibego 0.2.52__py3-none-any.whl → 1.0.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.
Potentially problematic release.
This version of vibego might be problematic. Click here for more details.
- bot.py +1557 -1431
- logging_setup.py +25 -18
- master.py +799 -508
- project_repository.py +42 -40
- scripts/__init__.py +1 -2
- scripts/bump_version.sh +57 -55
- scripts/log_writer.py +19 -16
- scripts/master_healthcheck.py +44 -44
- scripts/models/claudecode.sh +4 -4
- scripts/models/codex.sh +1 -1
- scripts/models/common.sh +24 -6
- scripts/models/gemini.sh +2 -2
- scripts/publish.sh +50 -50
- scripts/run_bot.sh +38 -17
- scripts/start.sh +136 -116
- scripts/start_tmux_codex.sh +8 -8
- scripts/stop_all.sh +21 -21
- scripts/stop_bot.sh +31 -10
- scripts/test_deps_check.sh +32 -28
- tasks/__init__.py +1 -1
- tasks/commands.py +4 -4
- tasks/constants.py +1 -1
- tasks/fsm.py +9 -9
- tasks/models.py +7 -7
- tasks/service.py +56 -56
- vibego-1.0.0.dist-info/METADATA +236 -0
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/RECORD +36 -35
- vibego-1.0.0.dist-info/licenses/LICENSE +201 -0
- vibego_cli/__init__.py +5 -4
- vibego_cli/__main__.py +1 -2
- vibego_cli/config.py +9 -9
- vibego_cli/deps.py +8 -9
- vibego_cli/main.py +63 -63
- vibego-0.2.52.dist-info/METADATA +0 -197
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/WHEEL +0 -0
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/entry_points.txt +0 -0
- {vibego-0.2.52.dist-info → vibego-1.0.0.dist-info}/top_level.txt +0 -0
scripts/stop_all.sh
CHANGED
|
@@ -17,10 +17,10 @@ VERBOSE=0
|
|
|
17
17
|
|
|
18
18
|
usage() {
|
|
19
19
|
cat <<USAGE
|
|
20
|
-
|
|
21
|
-
--dry-run
|
|
22
|
-
--verbose
|
|
23
|
-
-h, --help
|
|
20
|
+
usage:${0##*/} [--dry-run] [--verbose]
|
|
21
|
+
--dry-run Only prints what will be done, does not actually terminate the process
|
|
22
|
+
--verbose output more detailed diagnostic information
|
|
23
|
+
-h, --help Show this help
|
|
24
24
|
USAGE
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -53,7 +53,7 @@ parse_args() {
|
|
|
53
53
|
-h|--help)
|
|
54
54
|
usage; exit 0 ;;
|
|
55
55
|
*)
|
|
56
|
-
log_error "
|
|
56
|
+
log_error "unknown parameters: $1"
|
|
57
57
|
usage
|
|
58
58
|
exit 1 ;;
|
|
59
59
|
esac
|
|
@@ -70,10 +70,10 @@ terminate_pattern() {
|
|
|
70
70
|
local pids
|
|
71
71
|
pids=$(list_pids "$pattern")
|
|
72
72
|
if [[ -z "$pids" ]]; then
|
|
73
|
-
(( VERBOSE )) && log_info "
|
|
73
|
+
(( VERBOSE )) && log_info "not found $desc process"
|
|
74
74
|
return 0
|
|
75
75
|
fi
|
|
76
|
-
log_info "
|
|
76
|
+
log_info "stop $desc: $pids"
|
|
77
77
|
if (( DRY_RUN )); then
|
|
78
78
|
return 0
|
|
79
79
|
fi
|
|
@@ -82,31 +82,31 @@ terminate_pattern() {
|
|
|
82
82
|
local leftover
|
|
83
83
|
leftover=$(list_pids "$pattern")
|
|
84
84
|
if [[ -n "$leftover" ]]; then
|
|
85
|
-
log_warn "$desc
|
|
85
|
+
log_warn "$desc Incomplete exit, execute kill -9 $leftover"
|
|
86
86
|
kill -9 $leftover >/dev/null 2>&1 || true
|
|
87
87
|
fi
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
stop_workers() {
|
|
91
91
|
if [[ ! -x "$STOP_BOT_SCRIPT" ]]; then
|
|
92
|
-
log_warn "
|
|
92
|
+
log_warn "stop not found_bot.sh, Skip worker cleanup"
|
|
93
93
|
return 0
|
|
94
94
|
fi
|
|
95
95
|
if (( DRY_RUN )); then
|
|
96
|
-
log_info "[dry-run]
|
|
96
|
+
log_info "[dry-run] call stop_bot.sh Clean all workers"
|
|
97
97
|
return 0
|
|
98
98
|
fi
|
|
99
99
|
if "$STOP_BOT_SCRIPT" >/dev/null 2>&1; then
|
|
100
|
-
log_info "worker
|
|
100
|
+
log_info "worker alreadystop"
|
|
101
101
|
else
|
|
102
|
-
log_warn "stop_bot.sh
|
|
102
|
+
log_warn "stop_bot.sh Returns non-zero status, please check the logs"
|
|
103
103
|
fi
|
|
104
104
|
return 0
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
stop_tmux_sessions() {
|
|
108
108
|
if ! command -v tmux >/dev/null 2>&1; then
|
|
109
|
-
(( VERBOSE )) && log_info "
|
|
109
|
+
(( VERBOSE )) && log_info "tmux not detected, session cleanup skipped"
|
|
110
110
|
return 0
|
|
111
111
|
fi
|
|
112
112
|
local prefix="$DEFAULT_TMUX_PREFIX"
|
|
@@ -120,12 +120,12 @@ stop_tmux_sessions() {
|
|
|
120
120
|
local tmux_output sessions
|
|
121
121
|
tmux_output=$(tmux -u list-sessions 2>/dev/null || true)
|
|
122
122
|
if [[ -z "$tmux_output" ]]; then
|
|
123
|
-
(( VERBOSE )) && log_info "
|
|
123
|
+
(( VERBOSE )) && log_info "not foundmatch tmux session"
|
|
124
124
|
return 0
|
|
125
125
|
fi
|
|
126
126
|
sessions=$(printf '%s\n' "$tmux_output" | awk -F: -v prefix="$full_prefix" '$1 ~ "^" prefix {print $1}')
|
|
127
127
|
if [[ -z "$sessions" ]]; then
|
|
128
|
-
(( VERBOSE )) && log_info "
|
|
128
|
+
(( VERBOSE )) && log_info "not foundmatch tmux session"
|
|
129
129
|
return 0
|
|
130
130
|
fi
|
|
131
131
|
while IFS= read -r sess; do
|
|
@@ -134,7 +134,7 @@ stop_tmux_sessions() {
|
|
|
134
134
|
log_info "[dry-run] tmux -u kill-session -t $sess"
|
|
135
135
|
else
|
|
136
136
|
tmux -u kill-session -t "$sess" >/dev/null 2>&1 || true
|
|
137
|
-
log_info "
|
|
137
|
+
log_info "tmux session terminated: $sess"
|
|
138
138
|
fi
|
|
139
139
|
done <<<"$sessions"
|
|
140
140
|
return 0
|
|
@@ -150,26 +150,26 @@ cleanup_state_files() {
|
|
|
150
150
|
rm -f "$file"
|
|
151
151
|
fi
|
|
152
152
|
removed=1
|
|
153
|
-
log_info "
|
|
153
|
+
log_info "Cleaned status file: $file"
|
|
154
154
|
fi
|
|
155
155
|
done
|
|
156
156
|
if (( ! removed )) && (( VERBOSE )); then
|
|
157
|
-
log_info "
|
|
157
|
+
log_info "Stateless files need to be cleaned"
|
|
158
158
|
fi
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
main() {
|
|
162
162
|
parse_args "$@"
|
|
163
|
-
(( DRY_RUN )) && log_info "
|
|
163
|
+
(( DRY_RUN )) && log_info "Dry-run mode: no processes will be terminated"
|
|
164
164
|
|
|
165
165
|
stop_workers
|
|
166
166
|
stop_tmux_sessions
|
|
167
167
|
terminate_pattern "[Pp]ython.*master.py" "master"
|
|
168
168
|
terminate_pattern "$ROOT_DIR/start.sh" "start.sh"
|
|
169
|
-
terminate_pattern "$ROOT_DIR/bot.py" "
|
|
169
|
+
terminate_pattern "$ROOT_DIR/bot.py" "residual worker"
|
|
170
170
|
cleanup_state_files
|
|
171
171
|
|
|
172
|
-
log_info "
|
|
172
|
+
log_info "All related processes and state cleaned up"
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
main "$@"
|
scripts/stop_bot.sh
CHANGED
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
+
# shellcheck disable=SC2155
|
|
4
5
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
|
|
7
|
+
resolve_config_root() {
|
|
8
|
+
local raw=""
|
|
9
|
+
if [[ -n "${MASTER_CONFIG_ROOT:-}" ]]; then
|
|
10
|
+
raw="$MASTER_CONFIG_ROOT"
|
|
11
|
+
elif [[ -n "${VIBEGO_CONFIG_DIR:-}" ]]; then
|
|
12
|
+
raw="$VIBEGO_CONFIG_DIR"
|
|
13
|
+
elif [[ -n "${XDG_CONFIG_HOME:-}" ]]; then
|
|
14
|
+
raw="${XDG_CONFIG_HOME%/}/vibego"
|
|
15
|
+
else
|
|
16
|
+
raw="$HOME/.config/vibego"
|
|
17
|
+
fi
|
|
18
|
+
if [[ "$raw" == ~* ]]; then
|
|
19
|
+
printf '%s' "${raw/#\~/$HOME}"
|
|
20
|
+
else
|
|
21
|
+
printf '%s' "$raw"
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
CONFIG_ROOT="$(resolve_config_root)"
|
|
26
|
+
LOG_ROOT="${LOG_ROOT:-$CONFIG_ROOT/logs}"
|
|
5
27
|
MODELS_DIR="$ROOT_DIR/scripts/models"
|
|
6
|
-
LOG_ROOT="${LOG_ROOT:-$ROOT_DIR/logs}"
|
|
7
28
|
MODEL_DEFAULT="${MODEL_DEFAULT:-codex}"
|
|
8
29
|
PROJECT_DEFAULT="${PROJECT_NAME:-}"
|
|
9
30
|
|
|
10
31
|
usage() {
|
|
11
32
|
cat <<USAGE
|
|
12
|
-
|
|
13
|
-
--model
|
|
14
|
-
--project
|
|
33
|
+
usage:${0##*/} [--model name] [--project name]
|
|
34
|
+
--model target model, default $MODEL_DEFAULT
|
|
35
|
+
--project Project alias; attempts to use the current directory configuration when not specified
|
|
15
36
|
USAGE
|
|
16
37
|
}
|
|
17
38
|
|
|
@@ -27,7 +48,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
27
48
|
-h|--help)
|
|
28
49
|
usage; exit 0 ;;
|
|
29
50
|
*)
|
|
30
|
-
echo "
|
|
51
|
+
echo "unknown parameters: $1" >&2
|
|
31
52
|
usage
|
|
32
53
|
exit 1 ;;
|
|
33
54
|
esac
|
|
@@ -73,7 +94,7 @@ graceful_shutdown_claudecode() {
|
|
|
73
94
|
done
|
|
74
95
|
(( has_claude )) || return 0
|
|
75
96
|
|
|
76
|
-
printf '[stop-bot]
|
|
97
|
+
printf '[stop-bot] ClaudeCode session detected, trying to send /exit (session=%s)\n' "$session"
|
|
77
98
|
for pane in "${pane_ids[@]}"; do
|
|
78
99
|
tmux -u send-keys -t "$pane" Escape
|
|
79
100
|
tmux -u send-keys -t "$pane" C-u
|
|
@@ -91,11 +112,11 @@ graceful_shutdown_claudecode() {
|
|
|
91
112
|
fi
|
|
92
113
|
done
|
|
93
114
|
if (( still_running == 0 )); then
|
|
94
|
-
printf '[stop-bot] ClaudeCode
|
|
115
|
+
printf '[stop-bot] ClaudeCode Session has responded /exit (session=%s)\n' "$session"
|
|
95
116
|
break
|
|
96
117
|
fi
|
|
97
118
|
if (( $(date +%s) >= end_time )); then
|
|
98
|
-
printf '[stop-bot] ClaudeCode /exit
|
|
119
|
+
printf '[stop-bot] ClaudeCode /exit Timeout, forced shutdown will continue (session=%s)\n' "$session" >&2
|
|
99
120
|
break
|
|
100
121
|
fi
|
|
101
122
|
sleep 0.5
|
|
@@ -194,11 +215,11 @@ if [[ -n "$PROJECT_OVERRIDE" ]]; then
|
|
|
194
215
|
stop_single_worker "$PROJECT_NAME" "$MODEL"
|
|
195
216
|
else
|
|
196
217
|
if ! stop_all_workers; then
|
|
197
|
-
# fallback
|
|
218
|
+
# fallback:default project name
|
|
198
219
|
stop_single_worker "project" "$MODEL"
|
|
199
220
|
fi
|
|
200
221
|
fi
|
|
201
222
|
|
|
202
|
-
#
|
|
223
|
+
# The process has been stopped by project through the pid file and tmux session. No additional global pkill is needed to avoid accidentally killing other projects.
|
|
203
224
|
|
|
204
225
|
exit 0
|
scripts/test_deps_check.sh
CHANGED
|
@@ -1,66 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
2
|
+
# Test the logic of the dependency check function
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
|
|
5
5
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
MASTER_CONFIG_ROOT="${MASTER_CONFIG_ROOT:-$HOME/.config/vibego}"
|
|
7
|
+
RUNTIME_DIR="${VIBEGO_RUNTIME_ROOT:-$MASTER_CONFIG_ROOT/runtime}"
|
|
8
|
+
VENV_DIR="$RUNTIME_DIR/.venv"
|
|
6
9
|
|
|
7
|
-
echo "===
|
|
10
|
+
echo "=== Test dependency check function ==="
|
|
8
11
|
echo "ROOT_DIR: $ROOT_DIR"
|
|
12
|
+
echo "RUNTIME_DIR: $RUNTIME_DIR"
|
|
9
13
|
echo ""
|
|
10
14
|
|
|
11
|
-
#
|
|
15
|
+
# from start.shExtract check_deps_installedfunction to test
|
|
12
16
|
check_deps_installed() {
|
|
13
|
-
if [[ ! -d "$
|
|
14
|
-
echo "
|
|
17
|
+
if [[ ! -d "$VENV_DIR" ]]; then
|
|
18
|
+
echo "ERROR: Virtual environment does not exist"
|
|
15
19
|
return 1
|
|
16
20
|
fi
|
|
17
21
|
|
|
18
|
-
if [[ ! -x "$
|
|
19
|
-
echo "
|
|
22
|
+
if [[ ! -x "$VENV_DIR/bin/python" ]]; then
|
|
23
|
+
echo "ERROR: Python interpreter not found in virtual environment"
|
|
20
24
|
return 1
|
|
21
25
|
fi
|
|
22
26
|
|
|
23
|
-
if ! "$
|
|
24
|
-
echo "
|
|
27
|
+
if ! "$VENV_DIR/bin/python" -c "import aiogram, aiohttp, aiosqlite" 2>/dev/null; then
|
|
28
|
+
echo "ERROR: Required dependencies are missing or broken"
|
|
25
29
|
return 1
|
|
26
30
|
fi
|
|
27
31
|
|
|
28
|
-
echo "
|
|
32
|
+
echo "OK: Dependency check passed"
|
|
29
33
|
return 0
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
#
|
|
33
|
-
echo "
|
|
36
|
+
# Test 1: Check the current environment
|
|
37
|
+
echo "[Test 1] Check the current virtual environment status:"
|
|
34
38
|
if check_deps_installed; then
|
|
35
|
-
echo "
|
|
39
|
+
echo "Result: dependencies are complete, pip install will be skipped when restarting"
|
|
36
40
|
else
|
|
37
|
-
echo "
|
|
41
|
+
echo "Result: Dependencies are missing, pip install will be executed when restarting"
|
|
38
42
|
fi
|
|
39
43
|
echo ""
|
|
40
44
|
|
|
41
|
-
#
|
|
42
|
-
echo "
|
|
43
|
-
if [[ -d "$
|
|
44
|
-
echo "
|
|
45
|
-
ls -lh "$
|
|
45
|
+
# Test 2: Check the virtual environment directory
|
|
46
|
+
echo "[Test 2] Virtual environment directory check:"
|
|
47
|
+
if [[ -d "$VENV_DIR" ]]; then
|
|
48
|
+
echo "OK: Virtual environment directory exists: $VENV_DIR"
|
|
49
|
+
ls -lh "$VENV_DIR/bin/python" 2>/dev/null || echo "ERROR: Python interpreter does not exist"
|
|
46
50
|
else
|
|
47
|
-
echo "
|
|
51
|
+
echo "ERROR: Virtual environment directory does not exist: $VENV_DIR"
|
|
48
52
|
fi
|
|
49
53
|
echo ""
|
|
50
54
|
|
|
51
|
-
#
|
|
52
|
-
echo "
|
|
53
|
-
if [[ -x "$
|
|
55
|
+
# Test 3: Check critical dependencies
|
|
56
|
+
echo "[Test 3] Key dependency package check:"
|
|
57
|
+
if [[ -x "$VENV_DIR/bin/python" ]]; then
|
|
54
58
|
for pkg in aiogram aiohttp aiosqlite; do
|
|
55
|
-
if "$
|
|
56
|
-
echo "
|
|
59
|
+
if "$VENV_DIR/bin/python" -c "import $pkg" 2>/dev/null; then
|
|
60
|
+
echo "OK: $pkg installed"
|
|
57
61
|
else
|
|
58
|
-
echo "
|
|
62
|
+
echo "ERROR: $pkg not installed"
|
|
59
63
|
fi
|
|
60
64
|
done
|
|
61
65
|
else
|
|
62
|
-
echo "
|
|
66
|
+
echo "WARN: Unable to check (Python interpreter unavailable)"
|
|
63
67
|
fi
|
|
64
68
|
echo ""
|
|
65
69
|
|
|
66
|
-
echo "===
|
|
70
|
+
echo "=== Test completed ==="
|
tasks/__init__.py
CHANGED
tasks/commands.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Utilities for parsing command-style text payloads."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import re
|
|
@@ -9,7 +9,7 @@ ESCAPED_PIPE_RE = re.compile(r"\\\|")
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def _split_segments(raw: str) -> list[str]:
|
|
12
|
-
"""
|
|
12
|
+
"""Split by unescaped ``|`` characters and restore escaped pipes."""
|
|
13
13
|
|
|
14
14
|
if not raw:
|
|
15
15
|
return []
|
|
@@ -35,7 +35,7 @@ def _split_segments(raw: str) -> list[str]:
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def parse_structured_text(raw: str) -> Tuple[str, Dict[str, str]]:
|
|
38
|
-
"""
|
|
38
|
+
"""Parse command arguments and return the body together with key/value fields."""
|
|
39
39
|
|
|
40
40
|
segments = _split_segments(raw)
|
|
41
41
|
if not segments:
|
|
@@ -54,7 +54,7 @@ def parse_structured_text(raw: str) -> Tuple[str, Dict[str, str]]:
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def parse_simple_kv(raw: str) -> Dict[str, str]:
|
|
57
|
-
"""
|
|
57
|
+
"""Parse only ``key=value`` segments and ignore the body text."""
|
|
58
58
|
|
|
59
59
|
_, extra = parse_structured_text(raw)
|
|
60
60
|
return extra
|
tasks/constants.py
CHANGED
tasks/fsm.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""aiogram FSM
|
|
1
|
+
"""aiogram FSM state definitions."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from aiogram.fsm.state import State, StatesGroup
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class TaskCreateStates(StatesGroup):
|
|
8
|
-
"""
|
|
8
|
+
"""States for the guided task creation flow."""
|
|
9
9
|
|
|
10
10
|
waiting_title = State()
|
|
11
11
|
waiting_type = State()
|
|
@@ -14,7 +14,7 @@ class TaskCreateStates(StatesGroup):
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class TaskEditStates(StatesGroup):
|
|
17
|
-
"""
|
|
17
|
+
"""States for editing existing tasks."""
|
|
18
18
|
|
|
19
19
|
waiting_task_id = State()
|
|
20
20
|
waiting_field_choice = State()
|
|
@@ -23,7 +23,7 @@ class TaskEditStates(StatesGroup):
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class TaskNoteStates(StatesGroup):
|
|
26
|
-
"""
|
|
26
|
+
"""States for appending notes to a task."""
|
|
27
27
|
|
|
28
28
|
waiting_task_id = State()
|
|
29
29
|
waiting_content = State()
|
|
@@ -31,7 +31,7 @@ class TaskNoteStates(StatesGroup):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class TaskBugReportStates(StatesGroup):
|
|
34
|
-
"""
|
|
34
|
+
"""States for the defect reporting flow."""
|
|
35
35
|
|
|
36
36
|
waiting_description = State()
|
|
37
37
|
waiting_reproduction = State()
|
|
@@ -40,26 +40,26 @@ class TaskBugReportStates(StatesGroup):
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
class TaskDescriptionStates(StatesGroup):
|
|
43
|
-
"""
|
|
43
|
+
"""States for editing task descriptions."""
|
|
44
44
|
|
|
45
45
|
waiting_content = State()
|
|
46
46
|
waiting_confirm = State()
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class TaskPushStates(StatesGroup):
|
|
50
|
-
"""
|
|
50
|
+
"""States for sending supplemental task updates."""
|
|
51
51
|
|
|
52
52
|
waiting_choice = State()
|
|
53
53
|
waiting_supplement = State()
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
class TaskListSearchStates(StatesGroup):
|
|
57
|
-
"""
|
|
57
|
+
"""States for searching within the task list."""
|
|
58
58
|
|
|
59
59
|
waiting_keyword = State()
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
class ProjectDeleteStates(StatesGroup):
|
|
63
|
-
"""
|
|
63
|
+
"""States for confirming master project deletion."""
|
|
64
64
|
|
|
65
65
|
confirming = State()
|
tasks/models.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Data models used by the task subsystem."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
@@ -14,7 +14,7 @@ except ZoneInfoNotFoundError:
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _format_shanghai(dt: datetime) -> str:
|
|
17
|
-
"""
|
|
17
|
+
"""Format a datetime as an ISO8601 string in the Shanghai timezone."""
|
|
18
18
|
|
|
19
19
|
if dt.tzinfo is None:
|
|
20
20
|
dt = dt.replace(tzinfo=timezone.utc)
|
|
@@ -23,13 +23,13 @@ def _format_shanghai(dt: datetime) -> str:
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def shanghai_now_iso() -> str:
|
|
26
|
-
"""
|
|
26
|
+
"""Return the current time in Shanghai as an ISO8601 string."""
|
|
27
27
|
|
|
28
28
|
return _format_shanghai(datetime.now(SHANGHAI_TZ))
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def ensure_shanghai_iso(value: Optional[str]) -> Optional[str]:
|
|
32
|
-
"""
|
|
32
|
+
"""Normalize an ISO string to the Shanghai timezone representation."""
|
|
33
33
|
|
|
34
34
|
if value is None:
|
|
35
35
|
return None
|
|
@@ -44,7 +44,7 @@ def ensure_shanghai_iso(value: Optional[str]) -> Optional[str]:
|
|
|
44
44
|
|
|
45
45
|
@dataclass(slots=True)
|
|
46
46
|
class TaskRecord:
|
|
47
|
-
"""
|
|
47
|
+
"""Core fields that describe a single task."""
|
|
48
48
|
|
|
49
49
|
id: str
|
|
50
50
|
project_slug: str
|
|
@@ -66,7 +66,7 @@ class TaskRecord:
|
|
|
66
66
|
|
|
67
67
|
@dataclass(slots=True)
|
|
68
68
|
class TaskNoteRecord:
|
|
69
|
-
"""
|
|
69
|
+
"""Note information attached to a task."""
|
|
70
70
|
|
|
71
71
|
id: int
|
|
72
72
|
task_id: str
|
|
@@ -77,7 +77,7 @@ class TaskNoteRecord:
|
|
|
77
77
|
|
|
78
78
|
@dataclass(slots=True)
|
|
79
79
|
class TaskHistoryRecord:
|
|
80
|
-
"""
|
|
80
|
+
"""Historical trace of changes to task fields."""
|
|
81
81
|
|
|
82
82
|
id: int
|
|
83
83
|
task_id: str
|