dey 0.1.0__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.
- dey-0.1.0/PKG-INFO +6 -0
- dey-0.1.0/dey/__init__.py +1 -0
- dey-0.1.0/dey/__main__.py +3 -0
- dey-0.1.0/dey/cli.py +207 -0
- dey-0.1.0/dey.egg-info/PKG-INFO +6 -0
- dey-0.1.0/dey.egg-info/SOURCES.txt +10 -0
- dey-0.1.0/dey.egg-info/dependency_links.txt +1 -0
- dey-0.1.0/dey.egg-info/entry_points.txt +2 -0
- dey-0.1.0/dey.egg-info/requires.txt +1 -0
- dey-0.1.0/dey.egg-info/top_level.txt +1 -0
- dey-0.1.0/pyproject.toml +16 -0
- dey-0.1.0/setup.cfg +4 -0
dey-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
dey-0.1.0/dey/cli.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This time tracker is named after Alexander A. Dey
|
|
3
|
+
(https://americanhistory.si.edu/collections/object/nmah_856741).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from datetime import date, datetime
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
def print_status():
|
|
14
|
+
print("(status not yet implemented)")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def report_day(data, tails):
|
|
18
|
+
print(f"(day report: {tails})")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def report_week(data, tails):
|
|
22
|
+
print(f"(week report: {tails})")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def latest_entry(data):
|
|
26
|
+
"""
|
|
27
|
+
Return (date_key, entry_dict) for the most recent session, or (None, None).
|
|
28
|
+
"""
|
|
29
|
+
dates = [k for k in data if isinstance(k, date)]
|
|
30
|
+
if not dates:
|
|
31
|
+
return None, None
|
|
32
|
+
latest = max(dates)
|
|
33
|
+
entries = data[latest]
|
|
34
|
+
if not entries:
|
|
35
|
+
return None, None
|
|
36
|
+
return latest, entries[-1]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def toggle(data, tails):
|
|
40
|
+
# Resolve time and job from tails (order-independent)
|
|
41
|
+
time_str = None
|
|
42
|
+
job_name = None
|
|
43
|
+
for tail in tails:
|
|
44
|
+
if re.fullmatch(r"\d{1,2}:\d{2}", tail):
|
|
45
|
+
time_str = tail
|
|
46
|
+
else:
|
|
47
|
+
job_name = tail
|
|
48
|
+
|
|
49
|
+
if time_str is None:
|
|
50
|
+
time_str = datetime.now().strftime("%H:%M")
|
|
51
|
+
|
|
52
|
+
# Find the most recent session
|
|
53
|
+
latest_date, last_entry = latest_entry(data)
|
|
54
|
+
|
|
55
|
+
# Get the job ID, default to last.
|
|
56
|
+
if job_name is None:
|
|
57
|
+
if last_entry is None:
|
|
58
|
+
print("No job specified and no previous session.")
|
|
59
|
+
return
|
|
60
|
+
job_id = last_entry["job"]
|
|
61
|
+
else:
|
|
62
|
+
jobs = data.get("jobs", {})
|
|
63
|
+
job_id = jobs.get(job_name)
|
|
64
|
+
if job_id is None:
|
|
65
|
+
print(f"Job {job_name} is undefined.")
|
|
66
|
+
return
|
|
67
|
+
if job_id < 0:
|
|
68
|
+
print(f"Cannot use disabled job: {job_name}.")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Toggle: stop active session or start a new one.
|
|
72
|
+
if last_entry is not None and last_entry["stop"] is None:
|
|
73
|
+
# If active session exists, then stop it.
|
|
74
|
+
last_entry["stop"] = time_str
|
|
75
|
+
else:
|
|
76
|
+
# If no active session, then start a new one.
|
|
77
|
+
today = date.today()
|
|
78
|
+
data.setdefault(today, []).append({
|
|
79
|
+
"job": job_id,
|
|
80
|
+
"start": time_str,
|
|
81
|
+
"stop": None,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def adjust(data, tails):
|
|
86
|
+
print(f"(adjust: {tails})")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def name_job(data, tails):
|
|
90
|
+
if not tails:
|
|
91
|
+
return
|
|
92
|
+
new_name = tails[0]
|
|
93
|
+
jobs = data.setdefault("jobs", {})
|
|
94
|
+
|
|
95
|
+
if len(tails) > 1:
|
|
96
|
+
# Rename: find old name, update key
|
|
97
|
+
old_name = tails[1]
|
|
98
|
+
if old_name not in jobs:
|
|
99
|
+
print(f"Job {old_name} is undefined.")
|
|
100
|
+
return # reject
|
|
101
|
+
jobs[new_name] = jobs.pop(old_name)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
next_id = max((abs(v) for v in jobs.values()), default=0) + 1
|
|
105
|
+
jobs[new_name] = next_id
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def disable_job(data, tails):
|
|
109
|
+
jobs = data.setdefault("jobs", {})
|
|
110
|
+
job = tails[0]
|
|
111
|
+
if job in jobs:
|
|
112
|
+
jobs[job] = -abs(jobs[job])
|
|
113
|
+
else:
|
|
114
|
+
print(f"Job {job} is undefined.")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def enable_job(data, tails):
|
|
118
|
+
jobs = data.setdefault("jobs", {})
|
|
119
|
+
job = tails[0]
|
|
120
|
+
if job in jobs:
|
|
121
|
+
jobs[job] = abs(jobs[job])
|
|
122
|
+
else:
|
|
123
|
+
print(f"Job {job} is undefined.")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def undo(data, tails):
|
|
127
|
+
print(f"(undo: {tails})")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def print_help(data, tails):
|
|
131
|
+
print("You've chosen poorly!")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
FLAGS = {
|
|
135
|
+
"-r": (report_day, 2),
|
|
136
|
+
"-R": (report_week, 1),
|
|
137
|
+
"-c": (toggle, 2),
|
|
138
|
+
"-C": (adjust, 2),
|
|
139
|
+
"-j": (name_job, 2),
|
|
140
|
+
"-d": (disable_job, 1),
|
|
141
|
+
"-e": (enable_job, 1),
|
|
142
|
+
"-u": (undo, 1),
|
|
143
|
+
"-h": (print_help, 0),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def data_path() -> Path:
|
|
148
|
+
if sys.platform == "win32":
|
|
149
|
+
base = Path(os.environ["APPDATA"]) / "dey"
|
|
150
|
+
else:
|
|
151
|
+
base = Path(os.environ.get(
|
|
152
|
+
"XDG_CONFIG_HOME", Path.home() / ".config")) / "dey"
|
|
153
|
+
return base / "log.yaml"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def load_data() -> dict:
|
|
157
|
+
path = data_path()
|
|
158
|
+
if not path.exists():
|
|
159
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
path.touch()
|
|
161
|
+
return {}
|
|
162
|
+
with open(path) as f:
|
|
163
|
+
return yaml.safe_load(f) or {}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def save_data(data: dict) -> None:
|
|
167
|
+
path = data_path()
|
|
168
|
+
with open(path, "w") as f:
|
|
169
|
+
yaml.safe_dump(data, f,
|
|
170
|
+
default_flow_style=False, sort_keys=False)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
#def search_enabled(data: dict):
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def parse(argv):
|
|
177
|
+
"""Returns list of (flag, [tails]) in order."""
|
|
178
|
+
commands = []
|
|
179
|
+
i = 0
|
|
180
|
+
while i < len(argv):
|
|
181
|
+
token = argv[i]
|
|
182
|
+
if token not in FLAGS:
|
|
183
|
+
raise SystemExit(f"Unknown flag: {token}")
|
|
184
|
+
max_tails = FLAGS[token][1]
|
|
185
|
+
tails = []
|
|
186
|
+
i += 1
|
|
187
|
+
while i < len(argv) and len(tails) < max_tails \
|
|
188
|
+
and not argv[i].startswith("-"):
|
|
189
|
+
tails.append(argv[i])
|
|
190
|
+
i += 1
|
|
191
|
+
commands.append((token, tails))
|
|
192
|
+
return commands
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def main():
|
|
196
|
+
data = load_data()
|
|
197
|
+
commands = parse(sys.argv[1:])
|
|
198
|
+
if not commands:
|
|
199
|
+
print_status()
|
|
200
|
+
return
|
|
201
|
+
for flag, tails in commands:
|
|
202
|
+
FLAGS[flag][0](data, tails)
|
|
203
|
+
save_data(data)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyyaml>=6.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dey
|
dey-0.1.0/pyproject.toml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dey"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Terminal time tracker"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pyyaml>=6.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
dey = "dey.cli:main"
|
dey-0.1.0/setup.cfg
ADDED