Tonus 1.1.1__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.
- package/__init__.py +0 -0
- package/cli/__init__.py +0 -0
- package/cli/app.py +79 -0
- package/cli/main.py +13 -0
- package/data/__init__.py +0 -0
- package/data/init.py +76 -0
- package/data/project.py +105 -0
- package/data/record.py +162 -0
- package/entity/__init__.py +0 -0
- package/entity/project.py +14 -0
- package/entity/record.py +13 -0
- package/service/__init__.py +0 -0
- package/service/project/__init__.py +0 -0
- package/service/project/add.py +15 -0
- package/service/project/delete.py +7 -0
- package/service/project/extra.py +17 -0
- package/service/record/__init__.py +0 -0
- package/service/record/complete.py +14 -0
- package/service/record/show.py +105 -0
- package/service/record/start.py +34 -0
- package/service/record/stop.py +33 -0
- package/utils/utils.py +11 -0
- tonus-1.1.1.dist-info/METADATA +135 -0
- tonus-1.1.1.dist-info/RECORD +27 -0
- tonus-1.1.1.dist-info/WHEEL +5 -0
- tonus-1.1.1.dist-info/entry_points.txt +2 -0
- tonus-1.1.1.dist-info/top_level.txt +1 -0
package/__init__.py
ADDED
|
File without changes
|
package/cli/__init__.py
ADDED
|
File without changes
|
package/cli/app.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from package.service.record.start import start
|
|
3
|
+
from package.service.record.stop import stop
|
|
4
|
+
from package.service.record.show import show, list_all
|
|
5
|
+
from package.service.record.complete import complete_project_name
|
|
6
|
+
from package.service.project.add import add_project
|
|
7
|
+
from package.service.project.delete import delete_project
|
|
8
|
+
from package.service.project.extra import extra_time
|
|
9
|
+
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="to")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.callback(invoke_without_command=True)
|
|
16
|
+
def main(ctx: typer.Context):
|
|
17
|
+
if ctx.invoked_subcommand is None:
|
|
18
|
+
list_all()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command()
|
|
22
|
+
def open(
|
|
23
|
+
project: Annotated[
|
|
24
|
+
str, typer.Argument(help="project", shell_complete=complete_project_name)
|
|
25
|
+
],
|
|
26
|
+
task_desc: Annotated[str, typer.Option("--task", "-t", help="task")] = "",
|
|
27
|
+
extra_id: Annotated[str, typer.Option("--extra", "-e", help="extra_id")] = "",
|
|
28
|
+
):
|
|
29
|
+
start(project_name=project, task_desc=task_desc, extra_id=extra_id)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command()
|
|
33
|
+
def close(
|
|
34
|
+
project: Annotated[
|
|
35
|
+
str, typer.Argument(help="project", shell_complete=complete_project_name)
|
|
36
|
+
],
|
|
37
|
+
):
|
|
38
|
+
stop(project_name=project)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def check(
|
|
43
|
+
project: Annotated[
|
|
44
|
+
str, typer.Argument(help="project", shell_complete=complete_project_name)
|
|
45
|
+
],
|
|
46
|
+
):
|
|
47
|
+
show(project)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command()
|
|
51
|
+
def checkall():
|
|
52
|
+
list_all()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def add(
|
|
57
|
+
project: Annotated[str, typer.Argument(help="project")],
|
|
58
|
+
total: Annotated[int, typer.Option("--total", "-t", help="task")] = 156,
|
|
59
|
+
rate: Annotated[float, typer.Option("--extra", "-r", help="extra_id")] = 1.0,
|
|
60
|
+
):
|
|
61
|
+
add_project(project_name=project, total=total, rate=rate)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
def delete(
|
|
66
|
+
project: Annotated[
|
|
67
|
+
str, typer.Argument(help="project", shell_complete=complete_project_name)
|
|
68
|
+
],
|
|
69
|
+
):
|
|
70
|
+
delete_project(project_name=project)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command()
|
|
74
|
+
def extra(
|
|
75
|
+
project: Annotated[str, typer.Argument(help="project")],
|
|
76
|
+
extra_num: Annotated[int,typer.Argument(help="extra minute")] = 0,
|
|
77
|
+
invalid_day: Annotated[int,typer.Argument(help="invalid day")] = 1,
|
|
78
|
+
):
|
|
79
|
+
extra_time(project_name=project, minutes=extra_num, invalid_day=invalid_day)
|
package/cli/main.py
ADDED
package/data/__init__.py
ADDED
|
File without changes
|
package/data/init.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import functools
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_is_initialized = False
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DB_DIR = str(Path.home() / ".config" / "mana" / "data")
|
|
12
|
+
DB_PATH = os.path.join(DB_DIR, "fatigue.db")
|
|
13
|
+
CONFIG_PATH = os.path.join(DB_DIR, "config.json")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _init_db():
|
|
17
|
+
global _is_initialized
|
|
18
|
+
|
|
19
|
+
if _is_initialized:
|
|
20
|
+
return
|
|
21
|
+
conn = get_conn()
|
|
22
|
+
cursor = conn.cursor()
|
|
23
|
+
cursor.executescript(
|
|
24
|
+
"""
|
|
25
|
+
CREATE TABLE IF NOT EXISTS records (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
start INTEGER,
|
|
28
|
+
stop INTEGER,
|
|
29
|
+
duration INTEGER,
|
|
30
|
+
task_desc TEXT,
|
|
31
|
+
extra_id TEXT,
|
|
32
|
+
project_id INTEGER
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
project_name TEXT UNIQUE,
|
|
38
|
+
fatigue_rate REAL DEFAULT 1.0,
|
|
39
|
+
fatigue_total INTEGER DEFAULT 156,
|
|
40
|
+
project_status INTEGER DEFAULT 0,
|
|
41
|
+
extra_num INTEGER
|
|
42
|
+
);
|
|
43
|
+
"""
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
conn.commit()
|
|
47
|
+
conn.close()
|
|
48
|
+
_is_initialized = True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _create_config():
|
|
52
|
+
if os.path.exists(CONFIG_PATH):
|
|
53
|
+
return
|
|
54
|
+
else:
|
|
55
|
+
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
|
56
|
+
default_config = {"enable_timew": True}
|
|
57
|
+
json.dump(default_config, f, indent=4)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_conn():
|
|
61
|
+
if not os.path.exists(DB_DIR):
|
|
62
|
+
os.makedirs(DB_DIR, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
conn = sqlite3.connect(DB_PATH)
|
|
65
|
+
conn.row_factory = sqlite3.Row
|
|
66
|
+
return conn
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def ensure_db_ready(func):
|
|
70
|
+
@functools.wraps(func)
|
|
71
|
+
def wrapper(*args, **kargs):
|
|
72
|
+
_init_db()
|
|
73
|
+
_create_config()
|
|
74
|
+
return func(*args, **kargs)
|
|
75
|
+
|
|
76
|
+
return wrapper
|
package/data/project.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from package.data.init import ensure_db_ready, get_conn
|
|
2
|
+
from package.entity.project import Project
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@ensure_db_ready
|
|
6
|
+
def add_project(project: Project):
|
|
7
|
+
sql = "insert into projects(project_name,project_status,fatigue_rate,fatigue_total) values(?,?,?,?)"
|
|
8
|
+
with get_conn() as conn:
|
|
9
|
+
cursor = conn.cursor()
|
|
10
|
+
cursor.execute(
|
|
11
|
+
sql,
|
|
12
|
+
(
|
|
13
|
+
project.project_name,
|
|
14
|
+
project.project_status,
|
|
15
|
+
project.fatigue_rate,
|
|
16
|
+
project.fatigue_total,
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return cursor.lastrowid
|
|
21
|
+
|
|
22
|
+
@ensure_db_ready
|
|
23
|
+
def delete_project(project: str):
|
|
24
|
+
sql = "delete from projects where project_name = ?"
|
|
25
|
+
with get_conn() as conn:
|
|
26
|
+
cursor = conn.cursor()
|
|
27
|
+
cursor.execute(
|
|
28
|
+
sql,
|
|
29
|
+
(project,),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@ensure_db_ready
|
|
34
|
+
def update_project(project: Project):
|
|
35
|
+
project_dict = vars(project)
|
|
36
|
+
update_parts = {k: v for k,v in project_dict.items() if v is not None and k != "id"}
|
|
37
|
+
if not update_parts:
|
|
38
|
+
return None
|
|
39
|
+
update_keys = ",".join([f"{k} = ?" for k in update_parts.keys()])
|
|
40
|
+
sql = "update projects set " + update_keys + " where id = ?"
|
|
41
|
+
params = list(update_parts.values())
|
|
42
|
+
params.append(project.id)
|
|
43
|
+
|
|
44
|
+
with get_conn() as conn:
|
|
45
|
+
cursor = conn.cursor()
|
|
46
|
+
cursor.execute(sql, params)
|
|
47
|
+
conn.commit()
|
|
48
|
+
|
|
49
|
+
@ensure_db_ready
|
|
50
|
+
def batch_init_extra(project_ids: list[int]):
|
|
51
|
+
sql = "UPDATE projects SET extra_time = 0, invalid_time = 0, extra_start_time = 0 WHERE id = ?"
|
|
52
|
+
params = [(pid,) for pid in project_ids]
|
|
53
|
+
with get_conn() as conn:
|
|
54
|
+
cursor = conn.cursor()
|
|
55
|
+
cursor.executemany(sql, params)
|
|
56
|
+
conn.commit()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@ensure_db_ready
|
|
60
|
+
def get_project_id(id: int):
|
|
61
|
+
sql = "select id,project_name,fatigue_rate,fatigue_total,project_status from projects where id = ?"
|
|
62
|
+
with get_conn() as conn:
|
|
63
|
+
cursor = conn.cursor()
|
|
64
|
+
cursor.execute(
|
|
65
|
+
sql,
|
|
66
|
+
(id,),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return Project(**dict(cursor.fetchone()))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@ensure_db_ready
|
|
73
|
+
def get_project_name(project_name):
|
|
74
|
+
sql = "select id,project_name,fatigue_total,fatigue_rate,project_status,extra_time,extra_start_time,invalid_time from projects where project_name = ?"
|
|
75
|
+
with get_conn() as conn:
|
|
76
|
+
cursor = conn.cursor()
|
|
77
|
+
cursor.execute(
|
|
78
|
+
sql,
|
|
79
|
+
(project_name,),
|
|
80
|
+
)
|
|
81
|
+
row = cursor.fetchone()
|
|
82
|
+
if not row:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
return Project(**dict(row))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@ensure_db_ready
|
|
89
|
+
def get_all_projects():
|
|
90
|
+
sql = (
|
|
91
|
+
"select id,project_name,fatigue_total,fatigue_rate,project_status,invalid_time,extra_time,extra_start_time from projects"
|
|
92
|
+
)
|
|
93
|
+
with get_conn() as conn:
|
|
94
|
+
cursor = conn.cursor()
|
|
95
|
+
cursor.execute(
|
|
96
|
+
sql,
|
|
97
|
+
)
|
|
98
|
+
rows = cursor.fetchall()
|
|
99
|
+
if not rows:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
results = []
|
|
103
|
+
for r in rows:
|
|
104
|
+
results.append(Project(**dict(r)))
|
|
105
|
+
return results
|
package/data/record.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from package.data.init import ensure_db_ready, get_conn
|
|
2
|
+
from package.entity.record import Record
|
|
3
|
+
from package.entity.project import Project
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@ensure_db_ready
|
|
9
|
+
def add_record(record: Record):
|
|
10
|
+
sql = "insert into records(start,stop,duration,task_desc,extra_id,project_id) values(?,?,?,?,?,?)"
|
|
11
|
+
with get_conn() as conn:
|
|
12
|
+
cursor = conn.cursor()
|
|
13
|
+
cursor.execute(
|
|
14
|
+
sql,
|
|
15
|
+
(
|
|
16
|
+
record.start,
|
|
17
|
+
record.stop,
|
|
18
|
+
record.duration,
|
|
19
|
+
record.task_desc,
|
|
20
|
+
record.extra_id,
|
|
21
|
+
record.project_id,
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
conn.commit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@ensure_db_ready
|
|
28
|
+
def update_stop_record(record: Record):
|
|
29
|
+
sql = (
|
|
30
|
+
"update records set stop = ? , duration = ? where project_id = ? and stop = -1"
|
|
31
|
+
)
|
|
32
|
+
with get_conn() as conn:
|
|
33
|
+
cursor = conn.cursor()
|
|
34
|
+
cursor.execute(
|
|
35
|
+
sql,
|
|
36
|
+
(
|
|
37
|
+
record.stop,
|
|
38
|
+
record.duration,
|
|
39
|
+
record.project_id,
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
conn.commit()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@ensure_db_ready
|
|
46
|
+
def get_recent_pending_record_by_project(record: Record):
|
|
47
|
+
sql = (
|
|
48
|
+
"select id,start,stop,task_desc from records where stop = -1 and project_id = ?"
|
|
49
|
+
)
|
|
50
|
+
with get_conn() as conn:
|
|
51
|
+
cursor = conn.cursor()
|
|
52
|
+
cursor.execute(
|
|
53
|
+
sql,
|
|
54
|
+
(record.project_id,),
|
|
55
|
+
)
|
|
56
|
+
row = cursor.fetchone()
|
|
57
|
+
|
|
58
|
+
if not row:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
return Record(**dict(row))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@ensure_db_ready
|
|
65
|
+
def get_recent_record_by_project(record: Record):
|
|
66
|
+
sql = "select id,start,stop,task_desc from records where project_id = ? ORDER BY start DESC LIMIT 1"
|
|
67
|
+
with get_conn() as conn:
|
|
68
|
+
cursor = conn.cursor()
|
|
69
|
+
cursor.execute(
|
|
70
|
+
sql,
|
|
71
|
+
(record.project_id,),
|
|
72
|
+
)
|
|
73
|
+
row = cursor.fetchone()
|
|
74
|
+
|
|
75
|
+
if not row:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
return Record(**dict(row))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@ensure_db_ready
|
|
82
|
+
def get_projects():
|
|
83
|
+
sql = "select * from projects"
|
|
84
|
+
with get_conn() as conn:
|
|
85
|
+
cursor = conn.cursor()
|
|
86
|
+
cursor.execute(
|
|
87
|
+
sql
|
|
88
|
+
)
|
|
89
|
+
rows = cursor.fetchall()
|
|
90
|
+
|
|
91
|
+
if not rows:
|
|
92
|
+
return None
|
|
93
|
+
return [Project(**dict(row)) for row in rows]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@ensure_db_ready
|
|
97
|
+
def get_today_accumulated_fatigue(project_id=None):
|
|
98
|
+
now = datetime.now()
|
|
99
|
+
# 截止今天0点的时间戳
|
|
100
|
+
# 如果现在还没到凌晨 4 点,说明还在“昨天的那个统计周期”里
|
|
101
|
+
if now.hour < 4:
|
|
102
|
+
start_time = now.replace(hour=4, minute=0, second=0, microsecond=0) - timedelta(
|
|
103
|
+
days=1
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
# 否则,从今天的 4 点开始算
|
|
107
|
+
start_time = now.replace(hour=4, minute=0, second=0, microsecond=0)
|
|
108
|
+
|
|
109
|
+
start_ts = int(start_time.timestamp())
|
|
110
|
+
now_ts = int(time.time())
|
|
111
|
+
|
|
112
|
+
sql = """
|
|
113
|
+
SELECT p.id,
|
|
114
|
+
p.project_name,
|
|
115
|
+
p.fatigue_total,
|
|
116
|
+
SUM(
|
|
117
|
+
CASE
|
|
118
|
+
WHEN r.stop = -1 then (? - start)
|
|
119
|
+
ELSE r.duration
|
|
120
|
+
END * p.fatigue_rate / 60
|
|
121
|
+
) AS used_fatigue,
|
|
122
|
+
p.project_status,
|
|
123
|
+
p.extra_time,
|
|
124
|
+
p.invalid_time
|
|
125
|
+
FROM projects p
|
|
126
|
+
LEFT JOIN records r ON r.project_id = p.id AND r.start >= ? \
|
|
127
|
+
\
|
|
128
|
+
"""
|
|
129
|
+
params = [now_ts, start_ts]
|
|
130
|
+
if project_id:
|
|
131
|
+
sql = sql + " WHERE project_id = ?"
|
|
132
|
+
params.append(project_id)
|
|
133
|
+
|
|
134
|
+
sql += " GROUP BY p.id"
|
|
135
|
+
with get_conn() as conn:
|
|
136
|
+
cursor = conn.cursor()
|
|
137
|
+
cursor.execute(
|
|
138
|
+
sql,
|
|
139
|
+
tuple(params),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if project_id:
|
|
143
|
+
row = cursor.fetchone()
|
|
144
|
+
if row is not None:
|
|
145
|
+
total_duration = row[3] if row[3] is not None else 0
|
|
146
|
+
return total_duration
|
|
147
|
+
else:
|
|
148
|
+
return 0
|
|
149
|
+
else:
|
|
150
|
+
rows = cursor.fetchall()
|
|
151
|
+
results = []
|
|
152
|
+
for r in rows:
|
|
153
|
+
results.append(
|
|
154
|
+
{
|
|
155
|
+
"id": r[0],
|
|
156
|
+
"name": r[1],
|
|
157
|
+
"total": r[2] + r[5],
|
|
158
|
+
"used": r[3] if r[3] is not None else 0,
|
|
159
|
+
"status": r[4],
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
return results
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Project:
|
|
7
|
+
id: Optional[int]
|
|
8
|
+
project_name: Optional[str] = ""
|
|
9
|
+
fatigue_rate: Optional[float] = 1.0
|
|
10
|
+
fatigue_total: Optional[int] = 156
|
|
11
|
+
project_status: Optional[int] = 0,
|
|
12
|
+
extra_time: Optional[int] = 0,
|
|
13
|
+
invalid_time: Optional[int] = 0,
|
|
14
|
+
extra_start_time: Optional[int] = 0,
|
package/entity/record.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from package.entity.project import Project
|
|
2
|
+
from package.data.project import add_project as add_project_data
|
|
3
|
+
from package.data.init import ensure_db_ready
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@ensure_db_ready
|
|
7
|
+
def add_project(project_name, total, rate):
|
|
8
|
+
if project_name == "" or project_name is None:
|
|
9
|
+
raise ValueError("Project name cannot be empty")
|
|
10
|
+
|
|
11
|
+
project = Project(
|
|
12
|
+
None, project_name=project_name, fatigue_rate=rate, fatigue_total=total,project_status=3
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
add_project_data(project)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from package.data.project import update_project, get_project_name
|
|
4
|
+
from package.data.init import ensure_db_ready
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@ensure_db_ready
|
|
8
|
+
def extra_time(project_name, minutes:int, invalid_day:int):
|
|
9
|
+
invalid_time = invalid_day * 24 * 60 * 60 * 60
|
|
10
|
+
extra_start_time = int(datetime.now().timestamp())
|
|
11
|
+
if project_name == "" or project_name is None:
|
|
12
|
+
raise ValueError("Project name cannot be empty")
|
|
13
|
+
project = get_project_name(project_name=project_name)
|
|
14
|
+
project.extra_time = project.extra_time + minutes
|
|
15
|
+
project.invalid_time = invalid_time
|
|
16
|
+
project.extra_start_time = extra_start_time
|
|
17
|
+
update_project(project)
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from package.data.project import get_all_projects
|
|
2
|
+
import typer
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def complete_project_name(ctx: typer.Context, param, incomplete: str):
|
|
6
|
+
text = incomplete
|
|
7
|
+
projects = get_all_projects()
|
|
8
|
+
choices = [
|
|
9
|
+
p.project_name
|
|
10
|
+
for p in projects
|
|
11
|
+
if isinstance(p.project_name, str) and p.project_name.startswith(text)
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
return choices
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from package.data.project import get_project_name,get_all_projects,batch_init_extra
|
|
3
|
+
from package.data.record import (
|
|
4
|
+
get_today_accumulated_fatigue,
|
|
5
|
+
)
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.progress_bar import ProgressBar
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
status_map = {
|
|
13
|
+
0 : ("已暂停", "red"),
|
|
14
|
+
1 : ("进行中", "green"),
|
|
15
|
+
2 : ("已结束", "grey"),
|
|
16
|
+
3 : ("未开始", "yellow")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def show(project_name):
|
|
20
|
+
console = Console()
|
|
21
|
+
project = get_project_name(project_name=project_name)
|
|
22
|
+
|
|
23
|
+
if not project:
|
|
24
|
+
typer.secho("找不到此项目", fg=typer.colors.RED, err=True)
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
cost = int(get_today_accumulated_fatigue(project.id))
|
|
28
|
+
|
|
29
|
+
if cost > project.fatigue_total:
|
|
30
|
+
cost = project.fatigue_total
|
|
31
|
+
|
|
32
|
+
rest = (project.fatigue_total + project.extra_time) - cost
|
|
33
|
+
total = project.fatigue_total + project.extra_time
|
|
34
|
+
|
|
35
|
+
if rest < 0:
|
|
36
|
+
rest = 0
|
|
37
|
+
|
|
38
|
+
bar = ProgressBar(
|
|
39
|
+
total=total,
|
|
40
|
+
completed=rest,
|
|
41
|
+
width=40,
|
|
42
|
+
complete_style="yellow",
|
|
43
|
+
finished_style="green",
|
|
44
|
+
)
|
|
45
|
+
status_text, color = status_map.get(project.project_status,("未知","white"))
|
|
46
|
+
status = Text(status_text, style=color)
|
|
47
|
+
grid = Table.grid(padding=1)
|
|
48
|
+
grid.add_row(
|
|
49
|
+
Text(
|
|
50
|
+
f"{project.project_name}: {rest}/{total}:",
|
|
51
|
+
style="bold blue",
|
|
52
|
+
),
|
|
53
|
+
status,
|
|
54
|
+
bar,
|
|
55
|
+
Text(f"{rest / total:.1%}", style="magenta"),
|
|
56
|
+
)
|
|
57
|
+
console.print(grid)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def list_all():
|
|
61
|
+
p_ids = confirm_extra()
|
|
62
|
+
batch_init_extra(p_ids)
|
|
63
|
+
results = get_today_accumulated_fatigue()
|
|
64
|
+
console = Console()
|
|
65
|
+
if not results:
|
|
66
|
+
typer.secho("没有记录,请确认", fg=typer.colors.RED, err=True)
|
|
67
|
+
grid = Table.grid(padding=1)
|
|
68
|
+
grid.add_column(justify="right")
|
|
69
|
+
grid.add_column(justify="left")
|
|
70
|
+
grid.add_column(justify="right")
|
|
71
|
+
grid = Table.grid(expand=True, padding=(0, 1))
|
|
72
|
+
grid.add_column(no_wrap=True, justify="left")
|
|
73
|
+
grid.add_column(no_wrap=True, justify="right")
|
|
74
|
+
grid.add_column(no_wrap=True, justify="center", width=10)
|
|
75
|
+
grid.add_column(ratio=1)
|
|
76
|
+
grid.add_column(no_wrap=True, justify="right")
|
|
77
|
+
|
|
78
|
+
for r in results:
|
|
79
|
+
rest = max(0, int(r["total"] - r["used"]))
|
|
80
|
+
bar = ProgressBar(
|
|
81
|
+
total=r["total"],
|
|
82
|
+
completed=rest,
|
|
83
|
+
width=None, # 宽度自适应
|
|
84
|
+
complete_style="yellow",
|
|
85
|
+
finished_style="green",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
status_text, color = status_map.get(r["status"], ("未知", "white"))
|
|
89
|
+
status = Text(status_text, style=color)
|
|
90
|
+
grid.add_row(
|
|
91
|
+
Text(f"{r['id']} {r['name']}:", style="bold blue"),
|
|
92
|
+
Text(f"{rest}/{r['total']}:", style="bold blue"),
|
|
93
|
+
status,
|
|
94
|
+
bar,
|
|
95
|
+
Text(f"{rest / r['total']:.1%}", style="magenta")
|
|
96
|
+
)
|
|
97
|
+
console.print(grid)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def confirm_extra():
|
|
101
|
+
projects = get_all_projects()
|
|
102
|
+
current_stamp = datetime.now().timestamp()
|
|
103
|
+
filtered_projects = filter(lambda p: int(current_stamp) >= p.invalid_time + p.extra_start_time, projects)
|
|
104
|
+
project_ids = list(map(lambda p: p.id, filtered_projects))
|
|
105
|
+
return project_ids
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from package.data.project import get_project_name, add_project, update_project
|
|
2
|
+
from package.data.record import add_record, get_recent_pending_record_by_project
|
|
3
|
+
from package.entity.project import Project
|
|
4
|
+
from package.entity.record import Record
|
|
5
|
+
from package.utils.utils import get_config
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import typer
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def start(task_desc, project_name, extra_id):
|
|
12
|
+
p = get_project_name(project_name)
|
|
13
|
+
if not p:
|
|
14
|
+
p = Project(None, project_name=project_name, project_status=1)
|
|
15
|
+
p_id = add_project(p)
|
|
16
|
+
else:
|
|
17
|
+
p_id = p.id
|
|
18
|
+
|
|
19
|
+
record = get_recent_pending_record_by_project(Record(id=None, project_id=p_id))
|
|
20
|
+
if record:
|
|
21
|
+
typer.secho("项目已经进行,请先停止", fg=typer.colors.RED, err=True)
|
|
22
|
+
return
|
|
23
|
+
start_time = int(datetime.today().timestamp())
|
|
24
|
+
record = Record(
|
|
25
|
+
None, start=start_time, task_desc=task_desc, project_id=p_id, extra_id=extra_id
|
|
26
|
+
)
|
|
27
|
+
add_record(record=record)
|
|
28
|
+
p.project_status = 1
|
|
29
|
+
update_project(project=p)
|
|
30
|
+
|
|
31
|
+
config = get_config()
|
|
32
|
+
if config["enable_timew"]:
|
|
33
|
+
params = ["timew", "start", project_name]
|
|
34
|
+
subprocess.run(params)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from package.data.project import get_project_name, update_project
|
|
2
|
+
from package.data.record import get_recent_pending_record_by_project, update_stop_record
|
|
3
|
+
from package.entity.record import Record
|
|
4
|
+
from package.utils.utils import get_config
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import typer
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def stop(project_name):
|
|
11
|
+
project = get_project_name(project_name=project_name)
|
|
12
|
+
total_second = project.fatigue_total * 60
|
|
13
|
+
used_total_second = project.fatigue_total * total_second
|
|
14
|
+
if not project:
|
|
15
|
+
raise ValueError("找不到项目,请重新输入")
|
|
16
|
+
if not (record_ := get_recent_pending_record_by_project(Record(id=None, project_id=project.id))):
|
|
17
|
+
typer.secho("找不到正在进行的记录")
|
|
18
|
+
return
|
|
19
|
+
start_time = record_.start
|
|
20
|
+
if not start_time:
|
|
21
|
+
raise ValueError("找不到开始时间,请重新输入")
|
|
22
|
+
stop_time = int(datetime.today().timestamp())
|
|
23
|
+
duration_time = stop_time - start_time
|
|
24
|
+
record = Record(
|
|
25
|
+
id=None, stop=stop_time, duration=duration_time, project_id=project.id
|
|
26
|
+
)
|
|
27
|
+
update_stop_record(record)
|
|
28
|
+
project.project_status = 0 if used_total_second >= duration_time else 1
|
|
29
|
+
update_project(project=project)
|
|
30
|
+
config = get_config()
|
|
31
|
+
if config["enable_timew"]:
|
|
32
|
+
params = ["timew", "stop", project_name]
|
|
33
|
+
subprocess.run(params)
|
package/utils/utils.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
CONFIG_DIR = str(Path.home() / ".config" / "mana" / "data")
|
|
6
|
+
CONFIG_PATH = os.path.join(CONFIG_DIR, "config.json")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_config():
|
|
10
|
+
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
11
|
+
return json.load(f)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Tonus
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: a energy bar
|
|
5
|
+
Author-email: firefly <your.email@example.com>
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: typer>=0.9.0
|
|
15
|
+
Requires-Dist: rich
|
|
16
|
+
Requires-Dist: typing_extensions
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# Mana-bar (法力槽)
|
|
21
|
+
|
|
22
|
+
Mana-bar 是一个基于命令行的任务流管理工具,旨在帮助你管理不同项目的“精力”或“疲劳值”。通过设定每个项目的疲劳上限和消耗速率,你可以更好地规划和追踪你的工作状态,避免过度劳累。
|
|
23
|
+
|
|
24
|
+
## 简介
|
|
25
|
+
|
|
26
|
+
在日常工作中,我们往往会在多个项目之间切换。Mana-bar 引入了“法力值”(Mana)或“疲劳值”的概念:
|
|
27
|
+
- 每个项目都有一个**疲劳上限**(Total Fatigue)。
|
|
28
|
+
- 当你开始在一个项目上工作时,疲劳值会随着时间积累。
|
|
29
|
+
- 不同的项目可以设置不同的**消耗速率**(Rate)。
|
|
30
|
+
- 当疲劳值达到上限时,意味着你需要休息或切换到其他项目。
|
|
31
|
+
- 疲劳值会随着时间的推移(休息)通过某种机制恢复(当前代码逻辑主要关注记录和显示)。
|
|
32
|
+
|
|
33
|
+
## 安装
|
|
34
|
+
|
|
35
|
+
确保你已经安装了 Python 3.10+。
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone <repository_url>
|
|
39
|
+
cd mana_bar
|
|
40
|
+
pip install .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
或者使用 `pip` 安装依赖:
|
|
44
|
+
```bash
|
|
45
|
+
pip install -e .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 使用说明
|
|
49
|
+
|
|
50
|
+
安装完成后,你可以使用 `mana` 命令来管理你的任务。
|
|
51
|
+
|
|
52
|
+
### 1. 添加项目 (Add)
|
|
53
|
+
|
|
54
|
+
开始管理一个新项目之前,你需要先添加它,并设置其疲劳上限和消耗速率。
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
mana add <project_name> --total <total_fatigue> --rate <fatigue_rate>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `<project_name>`: 项目名称。
|
|
61
|
+
- `--total`, `-t`: 疲劳值上限,默认 156。
|
|
62
|
+
- `--rate`, `-r`: 疲劳消耗速率,默认 1.0。
|
|
63
|
+
|
|
64
|
+
示例:
|
|
65
|
+
```bash
|
|
66
|
+
mana add coding -t 200 -r 1.5
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. 开始任务 (Open)
|
|
70
|
+
|
|
71
|
+
开始在一个项目上工作。
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
mana open <project_name> [--task <task_desc>] [--extra <extra_id>]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- `<project_name>`: 项目名称。
|
|
78
|
+
- `--task`, `-t`: (可选) 任务描述。
|
|
79
|
+
- `--extra`, `-e`: (可选) 额外ID。
|
|
80
|
+
|
|
81
|
+
示例:
|
|
82
|
+
```bash
|
|
83
|
+
mana open coding -t "Implement README"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. 结束任务 (Close)
|
|
87
|
+
|
|
88
|
+
停止当前项目的计时。
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
mana close <project_name>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
示例:
|
|
95
|
+
```bash
|
|
96
|
+
mana close coding
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 4. 查看项目状态 (Check)
|
|
100
|
+
|
|
101
|
+
查看指定项目的当前疲劳值状态。
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
mana check <project_name>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
输出将显示一个进度条,指示当前的精力和疲劳状态。
|
|
108
|
+
|
|
109
|
+
### 5. 查看所有项目 (Checkall)
|
|
110
|
+
|
|
111
|
+
查看所有项目的状态。
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
mana checkall
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 开发
|
|
118
|
+
|
|
119
|
+
本项目使用 [Typer](https://typer.tiangolo.com/) 构建 CLI,使用 [Rich](https://github.com/Textualize/rich) 进行终端美化。
|
|
120
|
+
|
|
121
|
+
### 依赖安装
|
|
122
|
+
```bash
|
|
123
|
+
pip install -r requirements.txt
|
|
124
|
+
# 或者
|
|
125
|
+
pip install .[dev]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 运行测试
|
|
129
|
+
```bash
|
|
130
|
+
pytest
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
[MIT License](LICENSE)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
package/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
package/cli/app.py,sha256=0_IybNeDzmR7gwWQ-K4KuT7h0nda2HjvO_anlEGecSI,2168
|
|
4
|
+
package/cli/main.py,sha256=Jzas_XxgYU-vm-Ex2fe3i4R12lCLwW5YfAT6ur-ye3s,222
|
|
5
|
+
package/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
package/data/init.py,sha256=XU4OCYGLQ7J5hG4k4M9-g2JjP46slZQzkT392g9sNkQ,1726
|
|
7
|
+
package/data/project.py,sha256=wiGspEc7UNMwkPsDYax0wBRKWylaRF8jIx4YfnbAmS8,2996
|
|
8
|
+
package/data/record.py,sha256=hXLJjKQEI6IBr6mTogy6We6XzPHm0seGm1bAaEPDXpg,4451
|
|
9
|
+
package/entity/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
package/entity/project.py,sha256=l563mJBGHygb56P6_m3iytwoinaI5jHZvIceQD9GPBM,379
|
|
11
|
+
package/entity/record.py,sha256=zMWeQZdfX4-8KTc113Qh31lm3NEhZRKUl8kk2it35MY,245
|
|
12
|
+
package/service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
package/service/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
package/service/project/add.py,sha256=HZLYmPAoqZUIBTuPdMRSc1GcFBED4z-a2v5rLqkfQvE,483
|
|
15
|
+
package/service/project/delete.py,sha256=0yCb82cJ4Zdv5_E09m8UPOTKvRzKoUq8Fp_VSn4ypCo,208
|
|
16
|
+
package/service/project/extra.py,sha256=4qw27MWuwY3Wspja7vFR4xWQnWGIc9kjXQv88cJvXPE,664
|
|
17
|
+
package/service/record/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
package/service/record/complete.py,sha256=RY6iCiFIOfH2nYtJ957SI7vYedap3lhvDeSmqqFAJqQ,362
|
|
19
|
+
package/service/record/show.py,sha256=-2Yj6Suj6eI-GP8iCKbCmvS3IdnY4b1gHbaQlPoqPHU,3209
|
|
20
|
+
package/service/record/start.py,sha256=eTevB_u3QZfA6YYq7miLYyRqlyum7ae3lfFh5-dc7Ec,1173
|
|
21
|
+
package/service/record/stop.py,sha256=XAv9a8-kq1wvHXUXA4mrarJOKpqrO_sdqPQ945N4bZY,1345
|
|
22
|
+
package/utils/utils.py,sha256=SbXItH1cN9m4fW0knoNEVgL1irAwn_9AOFQGYS8WBn8,266
|
|
23
|
+
tonus-1.1.1.dist-info/METADATA,sha256=gw-XMuxobQR07iZzG7NX7LGHxA754rBNKFtxCejNJR8,3230
|
|
24
|
+
tonus-1.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
25
|
+
tonus-1.1.1.dist-info/entry_points.txt,sha256=pyhZtW8hvyIHvaaAY8zoMKhFnyHP2ZP3xE3uoAaVW5Q,45
|
|
26
|
+
tonus-1.1.1.dist-info/top_level.txt,sha256=TpQW7893uEAT8mPh-QCMZWhhKYvlqryC9Fjm5Hxg2f8,8
|
|
27
|
+
tonus-1.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
package
|