taskai-cli 1.0.1__tar.gz → 1.1.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.
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/.gitignore +4 -1
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/DEVLOG.md +23 -1
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/PKG-INFO +1 -1
- taskai_cli-1.1.1/migrations/_001_removing_lists.py +50 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/pyproject.toml +1 -1
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/taskai/cli.py +83 -13
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/taskai/config.py +2 -2
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/taskai/services/ai.py +4 -8
- taskai_cli-1.1.1/taskai/services/pomodoro.py +65 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/test/test_cli.py +20 -2
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/.github/workflows/publish-to-pypi.yml +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/README.md +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/docs/index.md +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/migrations/convert_title_to_name.py +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/mkdocs.yml +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/taskai/help_menu.py +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/taskai/json_dir_database.py +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/taskai/models.py +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/taskai/services/repair_database.py +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/taskai/services/user_setup.py +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/taskai/views.py +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/test/test_execution.py +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/test/test_json_dir_database.py +0 -0
- {taskai_cli-1.0.1 → taskai_cli-1.1.1}/test/test_view.py +0 -0
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
# 6-25
|
|
2
|
+
|
|
3
|
+
Alright let's start to think a bit about how I want to handle the pomo service.
|
|
4
|
+
|
|
5
|
+
task pomo 25 3
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Let's start with spawning the timer, and then we can worry about logging and shit.
|
|
9
|
+
|
|
10
|
+
Pomo should spawn a process and write a log to a database. The process
|
|
11
|
+
should continuously monitor that log and see "am I active?"
|
|
12
|
+
- if so, it keeps running
|
|
13
|
+
- if not, we prompt the user for what they want to do?
|
|
14
|
+
- rest/reset/cancel
|
|
15
|
+
do I need this to be serialized even? maybe not if I'm just starting - we can start pomo in another thread (or just the same thread)
|
|
16
|
+
|
|
17
|
+
Yeah let's honestly not even worry about the multiprocessing part of it right now.
|
|
18
|
+
|
|
19
|
+
Let's just enter a loop and count down, while I keep clearing the screen
|
|
20
|
+
|
|
21
|
+
I need to figure out how to make it play a bell or have a screen pop up (if it's running in the background)
|
|
22
|
+
|
|
1
23
|
# 6-20
|
|
2
24
|
|
|
3
25
|
Let's think critically about what i want this API to lookk like. How should be people be
|
|
@@ -365,4 +387,4 @@ Pages:
|
|
|
365
387
|
- Comments
|
|
366
388
|
- Text CLI
|
|
367
389
|
- Dockerize
|
|
368
|
-
-
|
|
390
|
+
-
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import orjson as json
|
|
3
|
+
from taskai.models import TodoItem
|
|
4
|
+
|
|
5
|
+
def migrate(data):
|
|
6
|
+
if "TodoList" in data:
|
|
7
|
+
for k, record in data["TodoList"].items():
|
|
8
|
+
|
|
9
|
+
record["child_ids"] = record.pop("item_ids")
|
|
10
|
+
TodoItem(**record)
|
|
11
|
+
print(f"Converting {k} to Item")
|
|
12
|
+
data["TodoItem"][k] = record
|
|
13
|
+
data.pop("TodoList")
|
|
14
|
+
|
|
15
|
+
for k, record in data["TodoItem"].items():
|
|
16
|
+
|
|
17
|
+
if "title" in record:
|
|
18
|
+
print(f"converting title->name for {k}")
|
|
19
|
+
record["title"] = record["name"]
|
|
20
|
+
record.pop("title")
|
|
21
|
+
|
|
22
|
+
if "parent" in record:
|
|
23
|
+
print(f"converting parent->parent_id for {k}")
|
|
24
|
+
record["parent_id"] = record["parent"]
|
|
25
|
+
record.pop("parent")
|
|
26
|
+
|
|
27
|
+
if "child_ids" not in record:
|
|
28
|
+
print(f"Adding Child IDs {k}")
|
|
29
|
+
record["child_ids"] = []
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
data["TodoItem"][k] = TodoItem(**record).model_dump()
|
|
33
|
+
except:
|
|
34
|
+
print("validation for {} failed".format(k))
|
|
35
|
+
import sys
|
|
36
|
+
sys.exit()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
arg_parser = argparse.ArgumentParser()
|
|
40
|
+
arg_parser.add_argument("path", help="path to taskai root")
|
|
41
|
+
args = arg_parser.parse_args()
|
|
42
|
+
|
|
43
|
+
with open(args.path, "rb") as f:
|
|
44
|
+
data = json.loads(f.read())
|
|
45
|
+
|
|
46
|
+
migrate(data)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
with open(args.path, "wb") as f:
|
|
50
|
+
f.write(json.dumps(data))
|
|
@@ -15,6 +15,7 @@ from taskai.models import TodoItem, Comment
|
|
|
15
15
|
from taskai.services.ai import ai_headstart_service, ai_natural_language_service
|
|
16
16
|
from taskai.services.user_setup import user_setup_service
|
|
17
17
|
from taskai.services.repair_database import repair_database_service
|
|
18
|
+
from taskai.services.pomodoro import pomodoro_service
|
|
18
19
|
from taskai.help_menu import help_menu
|
|
19
20
|
from taskai.config import GlobalConfig
|
|
20
21
|
|
|
@@ -208,13 +209,45 @@ class Controller:
|
|
|
208
209
|
db.commit()
|
|
209
210
|
|
|
210
211
|
# utilities
|
|
212
|
+
def _parse_arg_string(arg_string: str) -> list[str]:
|
|
213
|
+
"""parses a string properly before dispatching it to the argument parser"""
|
|
214
|
+
|
|
215
|
+
# scan for quote characters
|
|
216
|
+
|
|
217
|
+
currently_enclosed = False
|
|
218
|
+
current_quote_char = None
|
|
219
|
+
quote_chars = ('"',"'")
|
|
220
|
+
buffer = ""
|
|
221
|
+
arg_parts = []
|
|
222
|
+
for c in arg_string:
|
|
223
|
+
if c == " ":
|
|
224
|
+
if currently_enclosed:
|
|
225
|
+
buffer += c
|
|
226
|
+
else:
|
|
227
|
+
arg_parts.append(buffer)
|
|
228
|
+
buffer = ""
|
|
229
|
+
elif c == current_quote_char:
|
|
230
|
+
currently_enclosed = False
|
|
231
|
+
current_quote_char = None
|
|
232
|
+
|
|
233
|
+
elif c in quote_chars and not currently_enclosed:
|
|
234
|
+
currently_enclosed = True
|
|
235
|
+
current_quote_char = c
|
|
236
|
+
else:
|
|
237
|
+
buffer += c
|
|
238
|
+
if buffer:
|
|
239
|
+
arg_parts.append(buffer)
|
|
240
|
+
return arg_parts
|
|
241
|
+
|
|
211
242
|
def _parse_remaining(remaining_args: list[str]) -> tuple[list, dict]:
|
|
212
243
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
244
|
+
# parse flags
|
|
245
|
+
|
|
246
|
+
#for i, _arg in enumerate(remaining_args):
|
|
247
|
+
# remaining_args[i] = _arg.replace(" ", "+-*/")
|
|
248
|
+
#remaining_args = " ".join(remaining_args).replace("="," ").split(" ")
|
|
249
|
+
#for i, _arg in enumerate(remaining_args):
|
|
250
|
+
# remaining_args[i] = _arg.replace("+-*/", " ")
|
|
218
251
|
|
|
219
252
|
# outputs
|
|
220
253
|
args = []
|
|
@@ -223,11 +256,16 @@ def _parse_remaining(remaining_args: list[str]) -> tuple[list, dict]:
|
|
|
223
256
|
while remaining_args:
|
|
224
257
|
next_arg = remaining_args.pop(0)
|
|
225
258
|
if next_arg.startswith("--"):
|
|
226
|
-
|
|
227
|
-
|
|
259
|
+
if "=" in next_arg:
|
|
260
|
+
key, value = next_arg.split("=")
|
|
261
|
+
kwargs[key] = value
|
|
262
|
+
else:
|
|
263
|
+
assert remaining_args, "kwarg specified with no value provided"
|
|
264
|
+
kwargs[next_arg[2:]] = remaining_args.pop(0)
|
|
228
265
|
else:
|
|
229
266
|
args.append(next_arg)
|
|
230
|
-
|
|
267
|
+
|
|
268
|
+
|
|
231
269
|
return args, kwargs
|
|
232
270
|
|
|
233
271
|
def _is_int(val: any) -> bool:
|
|
@@ -325,7 +363,10 @@ def execute_commands(*args, **kwargs) -> int:
|
|
|
325
363
|
match (src_id, dst_id):
|
|
326
364
|
case _ if _is_int(src_id) and _is_int(dst_id): Controller.add_dependency(src_id, dst_id)
|
|
327
365
|
case _: Controller.throw_error("Invalid arrow argument, must be one of (->, <-)")
|
|
328
|
-
|
|
366
|
+
|
|
367
|
+
case "pomo":
|
|
368
|
+
pomodoro_service(int(args[1]), int(args[2]))
|
|
369
|
+
|
|
329
370
|
# developer use
|
|
330
371
|
case "db":
|
|
331
372
|
import orjson as json
|
|
@@ -349,22 +390,51 @@ def interactive_program():
|
|
|
349
390
|
# builtins.print = console.print
|
|
350
391
|
|
|
351
392
|
response = ""
|
|
393
|
+
last_show_command = [("show", "all"), {}]
|
|
394
|
+
args = None
|
|
395
|
+
kwargs = None
|
|
396
|
+
|
|
352
397
|
_clear_screen()
|
|
353
398
|
while True:
|
|
354
399
|
|
|
355
400
|
try:
|
|
401
|
+
|
|
402
|
+
# render last show command
|
|
403
|
+
if last_show_command is not None:
|
|
404
|
+
try:
|
|
405
|
+
execute_commands(*last_show_command[0], **last_show_command[1])
|
|
406
|
+
except Exception as e:
|
|
407
|
+
print(e)
|
|
408
|
+
|
|
409
|
+
Console().rule()
|
|
410
|
+
|
|
411
|
+
# prompt user input
|
|
356
412
|
response = Prompt.ask("Type your commands:", default=response)
|
|
357
|
-
|
|
358
|
-
|
|
413
|
+
|
|
414
|
+
# parse commands
|
|
415
|
+
args_remaining = _parse_arg_string(response)
|
|
416
|
+
args, kwargs = _parse_remaining(args_remaining)
|
|
417
|
+
print("arg_string:", args_remaining)
|
|
418
|
+
print("args:", args)
|
|
419
|
+
print("kwargs:", kwargs)
|
|
359
420
|
if args[0] == "task":
|
|
360
421
|
args = args[1:]
|
|
361
|
-
|
|
422
|
+
|
|
423
|
+
# defer show
|
|
424
|
+
if args[0] == "show":
|
|
425
|
+
last_show_command = (args, kwargs)
|
|
426
|
+
return_code = 1
|
|
427
|
+
else:
|
|
428
|
+
return_code = execute_commands(*args, **kwargs)
|
|
429
|
+
|
|
430
|
+
_clear_screen()
|
|
362
431
|
if return_code == 0:
|
|
363
432
|
break
|
|
364
|
-
Console().rule()
|
|
365
433
|
|
|
366
434
|
except KeyboardInterrupt:
|
|
367
435
|
break
|
|
436
|
+
except Exception as e:
|
|
437
|
+
print(e)
|
|
368
438
|
|
|
369
439
|
_clear_screen()
|
|
370
440
|
sys.exit(1)
|
|
@@ -7,9 +7,9 @@ class GlobalConfig:
|
|
|
7
7
|
_data_store: dict[str, any]
|
|
8
8
|
|
|
9
9
|
@classmethod
|
|
10
|
-
def load_dict(cls,
|
|
10
|
+
def load_dict(cls, model):
|
|
11
11
|
"""loads values from a configuration"""
|
|
12
|
-
cls._data_store =
|
|
12
|
+
cls._data_store = model.model_dump()
|
|
13
13
|
|
|
14
14
|
@classmethod
|
|
15
15
|
def get(cls, key: str):
|
|
@@ -30,8 +30,7 @@ def ai_headstart_service(
|
|
|
30
30
|
- Parse and return its response
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
item:TodoItem = db.read(item_id)
|
|
33
|
+
item:TodoItem = db.get_item(item_id)
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
from google import genai
|
|
@@ -67,11 +66,8 @@ task name: {item.name}
|
|
|
67
66
|
|
|
68
67
|
prompt += f"\ntask comments:"
|
|
69
68
|
for comment_id in item.comment_ids:
|
|
70
|
-
comment: Comment = db.
|
|
69
|
+
comment: Comment = db.get_comment(comment_id)
|
|
71
70
|
prompt += f"\n\t- {comment.content}"
|
|
72
|
-
|
|
73
|
-
# return "Survey says go fuck yourself"
|
|
74
|
-
|
|
75
71
|
# query model
|
|
76
72
|
client = genai.Client(api_key=api_key)
|
|
77
73
|
response = client.models.generate_content(
|
|
@@ -101,7 +97,7 @@ def ai_natural_language_service(
|
|
|
101
97
|
_visited_set = {}
|
|
102
98
|
|
|
103
99
|
def _add_info(item_id, level=0):
|
|
104
|
-
item: TodoItem = db.
|
|
100
|
+
item: TodoItem = db.get_item(id_)
|
|
105
101
|
user_info.append(" "*level + f"{item.id} {item.name}")
|
|
106
102
|
_visited_set.add(item_id)
|
|
107
103
|
if item.child_ids:
|
|
@@ -109,7 +105,7 @@ def ai_natural_language_service(
|
|
|
109
105
|
_add_info(child_id, level+1)
|
|
110
106
|
|
|
111
107
|
|
|
112
|
-
for id_ in db.
|
|
108
|
+
for id_ in db.get_item_ids():
|
|
113
109
|
if id_ not in _visited_set:
|
|
114
110
|
_add_info(id_)
|
|
115
111
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
2
|
+
import datetime
|
|
3
|
+
import time
|
|
4
|
+
from rich import print
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
def pomodoro_service(
|
|
8
|
+
minutes_on: int,
|
|
9
|
+
minutes_off: int
|
|
10
|
+
):
|
|
11
|
+
|
|
12
|
+
# STATES
|
|
13
|
+
end_time = datetime.datetime.now()
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
|
|
17
|
+
state = "ready"
|
|
18
|
+
history = []
|
|
19
|
+
console = Console()
|
|
20
|
+
while True:
|
|
21
|
+
if state == "ready":
|
|
22
|
+
prompt = "Press enter to begin ..."
|
|
23
|
+
input(prompt)
|
|
24
|
+
history.append(f"Starting Pomo for {minutes_on} minutes")
|
|
25
|
+
now = datetime.datetime.now()
|
|
26
|
+
end_time = now + datetime.timedelta(minutes=minutes_on)
|
|
27
|
+
#end_time = now + datetime.timedelta(seconds=5)
|
|
28
|
+
state = "running"
|
|
29
|
+
elif state == "running":
|
|
30
|
+
now = datetime.datetime.now()
|
|
31
|
+
if now < end_time:
|
|
32
|
+
diff = end_time - now
|
|
33
|
+
message = f"{diff.seconds // 60}:{str(diff.seconds % 60).zfill(2)} to go"
|
|
34
|
+
if history[-1].endswith("to go"):
|
|
35
|
+
history.pop()
|
|
36
|
+
history.append(message)
|
|
37
|
+
time.sleep(0.995)
|
|
38
|
+
else:
|
|
39
|
+
state = "done"
|
|
40
|
+
if state == "done":
|
|
41
|
+
history.append("All Finished! Taking rest now")
|
|
42
|
+
state = "resting"
|
|
43
|
+
end_time = datetime.datetime.now() + datetime.timedelta(minutes=minutes_off)
|
|
44
|
+
#end_time = now + datetime.timedelta(seconds=5)
|
|
45
|
+
if state == "resting":
|
|
46
|
+
now = datetime.datetime.now()
|
|
47
|
+
if now < end_time:
|
|
48
|
+
diff = end_time - now
|
|
49
|
+
message = f"{diff.seconds // 60}:{str(diff.seconds % 60).zfill(2)} to go"
|
|
50
|
+
if history[-1].endswith("to go"):
|
|
51
|
+
history.pop()
|
|
52
|
+
history.append(message)
|
|
53
|
+
time.sleep(0.995)
|
|
54
|
+
else:
|
|
55
|
+
state = "ready"
|
|
56
|
+
|
|
57
|
+
console.clear()
|
|
58
|
+
for val in history:
|
|
59
|
+
console.print(val)
|
|
60
|
+
except KeyboardInterrupt:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
2
|
+
from taskai.cli import _parse_arg_string
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
4
5
|
|
|
@@ -53,6 +54,23 @@ def test_run_commands():
|
|
|
53
54
|
finally:
|
|
54
55
|
_cleanup_db()
|
|
55
56
|
|
|
57
|
+
def test_args_parser():
|
|
58
|
+
|
|
59
|
+
def _assert_list_equal(l1, l2):
|
|
60
|
+
assert len(l1) == len(l2), f"{l1} length not equal to {l2} length"
|
|
61
|
+
assert all([thing1==thing2 for thing1, thing2 in zip(l1, l2)]), f"{l1} not equal to {l2}"
|
|
62
|
+
|
|
63
|
+
dataset = [
|
|
64
|
+
("show thing 1", ["show", "thing", "1"]),
|
|
65
|
+
("show 'this is a string' 2", ["show", "this is a string", "2"]),
|
|
66
|
+
("show https://url.com/thing?arg=1 hello there", ["show", "https://url.com/thing?arg=1", "hello", "there"])
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
for input, label in dataset:
|
|
70
|
+
output = _parse_arg_string(input)
|
|
71
|
+
_assert_list_equal(label, output)
|
|
72
|
+
|
|
73
|
+
|
|
56
74
|
|
|
57
75
|
def _cleanup_db():
|
|
58
76
|
os.chdir(CWD)
|
|
@@ -60,5 +78,5 @@ def _cleanup_db():
|
|
|
60
78
|
shutil.rmtree(TESTING_DIR)
|
|
61
79
|
|
|
62
80
|
if __name__ == "__main__":
|
|
63
|
-
|
|
64
|
-
test_run_commands()
|
|
81
|
+
test_args_parser()
|
|
82
|
+
# test_run_commands()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|