taskai-cli 0.1.3__tar.gz → 0.1.4__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-0.1.4/DEVLOG.md +285 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/PKG-INFO +1 -1
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/pyproject.toml +1 -1
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/taskai/cli.py +165 -48
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/taskai/json_dir_database.py +36 -8
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/taskai/models.py +6 -0
- taskai_cli-0.1.4/taskai/services/repair_database.py +39 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/taskai/views.py +13 -12
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/.github/workflows/publish-to-pypi.yml +0 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/.gitignore +0 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/README.md +0 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/docs/index.md +0 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/mkdocs.yml +0 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/taskai/config.py +0 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/taskai/help_menu.py +0 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/taskai/services/ai.py +0 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/taskai/services/user_setup.py +0 -0
- {taskai_cli-0.1.3 → taskai_cli-0.1.4}/test/test_json_dir_database.py +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# 6-14
|
|
2
|
+
|
|
3
|
+
Damn it's been a productive couple of weeks. App is deployed on PyPi and I'm starting to think
|
|
4
|
+
a bit more about phase 2, how to rewrite it for ultimate success. What I'm thinking is that
|
|
5
|
+
things like help, argument patterns, and service logics should be bundled into one unit -
|
|
6
|
+
|
|
7
|
+
the core infrastructure should basically load all of the services in the config, and pull their
|
|
8
|
+
argument pattern matching, help menus, etc. That way, the core of the app just works as a dispatcher
|
|
9
|
+
|
|
10
|
+
Things I need to have clearly in mind before I start rearchitecting:
|
|
11
|
+
- How i'm going to handle modularity and reuse for major components
|
|
12
|
+
- How i'm going to handle multiple threads and processes
|
|
13
|
+
- a note on this - i'm leaning towards having a core metadata file that just keeps track
|
|
14
|
+
of all processes, that we just read/write to (with some sort of locking mechanism). That
|
|
15
|
+
will maintain process ids and any other shared state
|
|
16
|
+
|
|
17
|
+
But before all that good stuff, let's take what we have and polish the hell out of it
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Also, as I'm doing this, it's becoming clear to me that I need to have records be a bit
|
|
21
|
+
more object oriented, as the number of different interactions that have to take place is
|
|
22
|
+
getting more and more annoying to keep track of and the best place for everything to be
|
|
23
|
+
put would be as methods on the objects, i.e. "item.updateList()", etc.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# 6-3
|
|
27
|
+
|
|
28
|
+
Alright so how are comments gonna look here:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Title:
|
|
32
|
+
Due By:
|
|
33
|
+
Description:
|
|
34
|
+
Depends On:
|
|
35
|
+
Comments
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
Let's think about what this cli refactor is gonna look like:
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
arg
|
|
44
|
+
arg
|
|
45
|
+
...
|
|
46
|
+
endpoint, *args, **kwargs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# 5-28
|
|
51
|
+
|
|
52
|
+
A couple things that I need to deal with:
|
|
53
|
+
- recursive deletes (so deleting hierarchical items)
|
|
54
|
+
- recursive completes
|
|
55
|
+
- better recurrence strategies
|
|
56
|
+
- how to deal with linking tasks that are recurrence of the same task
|
|
57
|
+
- database server
|
|
58
|
+
|
|
59
|
+
# 5-27
|
|
60
|
+
|
|
61
|
+
Let's think about this cli:
|
|
62
|
+
|
|
63
|
+
what do i want to be able to do?
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
- task add item|list|comment
|
|
69
|
+
- task show item|list|comment
|
|
70
|
+
- task complete {id}
|
|
71
|
+
- task delete {id}
|
|
72
|
+
- task {id} depends_on {id}
|
|
73
|
+
- task dependency_chain
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
- task {id} delete|done|show|edit
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
Let's start with some views
|
|
80
|
+
|
|
81
|
+
need to have recursive delete for lists
|
|
82
|
+
|
|
83
|
+
# 5-26
|
|
84
|
+
|
|
85
|
+
What can I build right now?
|
|
86
|
+
|
|
87
|
+
I want to have a basic CLI version of what I want. So we can use a pickle database,
|
|
88
|
+
define our models, and add the basic functionality to the core business logic.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
How can we design this?
|
|
92
|
+
|
|
93
|
+
- I could have a core app session that is built through some composition
|
|
94
|
+
- for different concurrent users, we could spin up different processes?
|
|
95
|
+
- why don't I worry about scaling when it comes time to scale, let's just build osmething cool and useful right now
|
|
96
|
+
|
|
97
|
+
- So we have the app session object, and we can spin it up:
|
|
98
|
+
- on startup
|
|
99
|
+
- connect to a database
|
|
100
|
+
- call methods to perform the actions we care about
|
|
101
|
+
- commit changes as we want
|
|
102
|
+
OR
|
|
103
|
+
- have a plugin architecture where we can add views/operations that take in the session object
|
|
104
|
+
- on exit
|
|
105
|
+
|
|
106
|
+
CLI version
|
|
107
|
+
- do I want it to run in the background and communicate via pipes?
|
|
108
|
+
- feels like that falls under premature optimization, maybe just try running everything at once to start
|
|
109
|
+
- though that could be fun? maybe next start
|
|
110
|
+
- maybe not a pickle database, can do a local folder json database
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
so:
|
|
115
|
+
Session:
|
|
116
|
+
def __init__(
|
|
117
|
+
user: User,
|
|
118
|
+
database: Database
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def on_exit():
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
cli/
|
|
125
|
+
views/ --> views of the core data
|
|
126
|
+
services/ --> core business operations we can perform
|
|
127
|
+
cli.py
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# 5-11
|
|
140
|
+
|
|
141
|
+
Alright let's think about backend
|
|
142
|
+
|
|
143
|
+
What is the point of this app? What do I want to be able to support?
|
|
144
|
+
-
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# 4-29
|
|
149
|
+
|
|
150
|
+
Alright let's think about frontend:
|
|
151
|
+
|
|
152
|
+
- Login Page:
|
|
153
|
+
- Page 1: login
|
|
154
|
+
- if logged in, show main
|
|
155
|
+
|
|
156
|
+
- Main Page
|
|
157
|
+
- SideBar
|
|
158
|
+
- MainPanel
|
|
159
|
+
- MainPanelOptions
|
|
160
|
+
|
|
161
|
+
- TodoPanel
|
|
162
|
+
- EditPanel
|
|
163
|
+
- CanvasPanel
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
What I want to do:
|
|
167
|
+
- build the threads part
|
|
168
|
+
- build the login and user context part
|
|
169
|
+
- build the database backend interactions
|
|
170
|
+
- build the canvas part
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
How do I want to handle components?
|
|
174
|
+
|
|
175
|
+
Brainstorm:
|
|
176
|
+
- I could construct a component tree
|
|
177
|
+
- each component could have a render call? Or just be called in its constructor, so when you instantiate
|
|
178
|
+
it, it shows up
|
|
179
|
+
- I want to have statically defined models so that I don't have to reconstitute things (since that's a pain)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
Alright yeah, components can also have a commit-esque method (although maybe that's case by case) where I
|
|
184
|
+
commit their data
|
|
185
|
+
|
|
186
|
+
In that sense, I don't even really need to predefine my HTML - I can just generate it on the fly and have my core
|
|
187
|
+
div structure be defined
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
So what would that look like?
|
|
191
|
+
|
|
192
|
+
<LoginForm>
|
|
193
|
+
<Main>
|
|
194
|
+
<div> flex-col
|
|
195
|
+
<TopPanel>
|
|
196
|
+
|
|
197
|
+
<div> flex-row
|
|
198
|
+
<SideBar>
|
|
199
|
+
<TodoPanel>
|
|
200
|
+
<EditPanel>
|
|
201
|
+
<CanvasPanel>
|
|
202
|
+
|
|
203
|
+
<footer>
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
I can just have so many classes:
|
|
208
|
+
components/
|
|
209
|
+
loginForm.js
|
|
210
|
+
sidebar.js
|
|
211
|
+
todopanel.js
|
|
212
|
+
editpanel.js
|
|
213
|
+
canvaspanel.js
|
|
214
|
+
|
|
215
|
+
lib/
|
|
216
|
+
component.js
|
|
217
|
+
database.js
|
|
218
|
+
utils.js
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
Alright let's start to sketch these out:
|
|
222
|
+
|
|
223
|
+
We have our base:
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class Component extends HTMLComponent{
|
|
227
|
+
|
|
228
|
+
db attribute
|
|
229
|
+
state attribute
|
|
230
|
+
|
|
231
|
+
constructor (
|
|
232
|
+
super()
|
|
233
|
+
const shadow = this.attachShadow({mode: 'open'})
|
|
234
|
+
customElements.define(this.constructor.name, this.constructor)
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class LoginForm extends Component {
|
|
240
|
+
constructor(
|
|
241
|
+
super()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# 4-27
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
Alright sick progress, I'm in the process of populating the database. Now I need to:
|
|
263
|
+
- write the startup() method that pulls lists from the database and populates them in the sidebar
|
|
264
|
+
---> I'll put the populateDb method in there for now
|
|
265
|
+
|
|
266
|
+
- write the callback so that when I click on a list, it pulls its id from the database
|
|
267
|
+
- write the rendering functions so that the todo-list-panel renders the todos for the selected list
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# 4-26
|
|
271
|
+
|
|
272
|
+
What do I want to add?
|
|
273
|
+
|
|
274
|
+
Pages:
|
|
275
|
+
- login page
|
|
276
|
+
- Main Page
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# Features that I want to add
|
|
280
|
+
|
|
281
|
+
- Pop Up Card Editing
|
|
282
|
+
- Comments
|
|
283
|
+
- Text CLI
|
|
284
|
+
- Dockerize
|
|
285
|
+
-
|
|
@@ -3,6 +3,9 @@ import argparse
|
|
|
3
3
|
import os
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
import builtins
|
|
6
|
+
import fnmatch
|
|
7
|
+
import sys
|
|
8
|
+
import subprocess
|
|
6
9
|
|
|
7
10
|
# local
|
|
8
11
|
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
@@ -10,11 +13,15 @@ from taskai.views import view_lists, view_item, view_items
|
|
|
10
13
|
from taskai.models import Base, TodoItem, TodoList, Comment
|
|
11
14
|
from taskai.services.ai import ai_headstart_service, ai_natural_language_service
|
|
12
15
|
from taskai.services.user_setup import user_setup_service
|
|
16
|
+
from taskai.services.repair_database import repair_database_service
|
|
13
17
|
from taskai.help_menu import help_menu
|
|
14
18
|
from taskai.config import GlobalConfig
|
|
15
19
|
|
|
16
20
|
# external
|
|
17
|
-
from rich import print
|
|
21
|
+
from rich import print, print_json
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
import rich
|
|
24
|
+
from rich.prompt import Prompt
|
|
18
25
|
|
|
19
26
|
# config
|
|
20
27
|
DB_PATH = ".taskai/task_db"
|
|
@@ -24,6 +31,7 @@ db = JsonDirectoryDatabase(
|
|
|
24
31
|
USER
|
|
25
32
|
)
|
|
26
33
|
db.connect()
|
|
34
|
+
GlobalConfig.load_dict(db.config)
|
|
27
35
|
|
|
28
36
|
og_help = builtins.help
|
|
29
37
|
def new_help(*args, **kwargs):
|
|
@@ -34,16 +42,20 @@ builtins.help = new_help
|
|
|
34
42
|
class Controller:
|
|
35
43
|
|
|
36
44
|
# utilities
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
def _find_model_by_stringmatch(attr: str, pattern: str) -> TodoItem|TodoList|Comment|None:
|
|
46
|
+
|
|
47
|
+
for record_type in [
|
|
48
|
+
TodoList,
|
|
49
|
+
TodoItem,
|
|
50
|
+
Comment
|
|
51
|
+
]:
|
|
52
|
+
batch_attrs = db.read_batch_attr(record_type, attr)
|
|
53
|
+
inside_out = {v: k for k, v in batch_attrs.items()} # TODO this is hacky
|
|
54
|
+
results = fnmatch.filter(batch_attrs.values(), pattern)
|
|
55
|
+
if results:
|
|
56
|
+
id_ = inside_out[results[0]] # might be duplication
|
|
57
|
+
return db.read(id_)
|
|
58
|
+
|
|
47
59
|
return None
|
|
48
60
|
|
|
49
61
|
|
|
@@ -52,25 +64,28 @@ class Controller:
|
|
|
52
64
|
print("kwargs:", kwargs)
|
|
53
65
|
|
|
54
66
|
# CRUD
|
|
55
|
-
def show_all(show_done=
|
|
67
|
+
def show_all(show_done=True):
|
|
56
68
|
view_lists(db, db.lists, show_done=show_done)
|
|
57
69
|
|
|
58
|
-
def show_by_id(id_, show_done=
|
|
70
|
+
def show_by_id(id_, show_done=True):
|
|
59
71
|
if id_ in db.items:
|
|
60
72
|
Controller.show_item(id_)
|
|
61
73
|
elif id_ in db.lists:
|
|
62
74
|
Controller.show_list(id_, show_done=show_done)
|
|
63
75
|
|
|
64
|
-
def show_by_list_name(value: str, show_done=
|
|
65
|
-
model = Controller.
|
|
76
|
+
def show_by_list_name(value: str, show_done=True):
|
|
77
|
+
model = Controller._find_model_by_stringmatch("name", value)
|
|
66
78
|
if isinstance(model, TodoList):
|
|
67
79
|
Controller.show_list(model.id, show_done=show_done)
|
|
68
80
|
else:
|
|
69
|
-
print(f"Could not find list
|
|
81
|
+
print(f"Could not find list matching pattern '{value}'")
|
|
70
82
|
|
|
71
|
-
def show_list(list_id: int|str, show_done=
|
|
83
|
+
def show_list(list_id: int|str, show_done=True):
|
|
72
84
|
view_lists(db, [list_id], show_done=show_done)
|
|
73
85
|
|
|
86
|
+
def show_lists():
|
|
87
|
+
view_lists(db, db.lists.keys(), show_items=False)
|
|
88
|
+
|
|
74
89
|
def show_item(item_id: int|str):
|
|
75
90
|
view_item(db, item_id)
|
|
76
91
|
|
|
@@ -92,7 +107,7 @@ class Controller:
|
|
|
92
107
|
try:
|
|
93
108
|
int(list_id)
|
|
94
109
|
except ValueError:
|
|
95
|
-
list_ = Controller.
|
|
110
|
+
list_ = Controller._find_model_by_stringmatch("name", list_id)
|
|
96
111
|
# TODO this should be just lists, not models
|
|
97
112
|
list_id = list_.id
|
|
98
113
|
|
|
@@ -104,7 +119,7 @@ class Controller:
|
|
|
104
119
|
match k:
|
|
105
120
|
case "completed": item.completed = bool(v)
|
|
106
121
|
case "description": item.description = str(v)
|
|
107
|
-
case "due_by": item.due_by = datetime.strptime(v, "%
|
|
122
|
+
case "due_by": item.due_by = datetime.strptime(v, "%m-%d-%Y")
|
|
108
123
|
case "parent": item.parent = str(v)
|
|
109
124
|
case "priority": item.priority = int(v)
|
|
110
125
|
case "depends_on": item.dependency_ids.extend(v.split(","))
|
|
@@ -130,7 +145,7 @@ class Controller:
|
|
|
130
145
|
case "list_id": item.list_id = str(v)
|
|
131
146
|
case "completed": item.completed = bool(v)
|
|
132
147
|
case "description": item.description = str(v)
|
|
133
|
-
case "due_by": item.due_by = datetime.strptime(v, "%
|
|
148
|
+
case "due_by": item.due_by = datetime.strptime(v, "%m-%d-%Y")
|
|
134
149
|
case "parent": item.parent = str(v)
|
|
135
150
|
case "priority": item.priority = int(v)
|
|
136
151
|
# TODO handle recurrence
|
|
@@ -143,6 +158,15 @@ class Controller:
|
|
|
143
158
|
db.commit()
|
|
144
159
|
print(f"Deleted {id_}")
|
|
145
160
|
|
|
161
|
+
def delete_list_by_name(name: str):
|
|
162
|
+
list_ = Controller._find_model_by_stringmatch("name", name)
|
|
163
|
+
if list_:
|
|
164
|
+
db.delete(list_.id)
|
|
165
|
+
db.commit()
|
|
166
|
+
else:
|
|
167
|
+
Controller.throw_error("Cannot find list by name")
|
|
168
|
+
|
|
169
|
+
|
|
146
170
|
def delete_completed():
|
|
147
171
|
for item_id in db.items.copy():
|
|
148
172
|
item: TodoItem = db.read(item_id)
|
|
@@ -166,7 +190,7 @@ class Controller:
|
|
|
166
190
|
print(db.get_config_value(key))
|
|
167
191
|
|
|
168
192
|
def list_config():
|
|
169
|
-
for k, v in db.get_config().items():
|
|
193
|
+
for k, v in db.get_config().model_dump().items():
|
|
170
194
|
print(f"{k}={v}")
|
|
171
195
|
|
|
172
196
|
def set_config_value(key: str, value: any):
|
|
@@ -181,6 +205,35 @@ class Controller:
|
|
|
181
205
|
def run_setup_service():
|
|
182
206
|
user_setup_service(db)
|
|
183
207
|
|
|
208
|
+
def repair_service():
|
|
209
|
+
repair_database_service(db)
|
|
210
|
+
|
|
211
|
+
def move_item(item_id: int|str, list_identifier: int|str):
|
|
212
|
+
|
|
213
|
+
if item_id not in db.items:
|
|
214
|
+
Controller.throw_error(f"Couldn't find items with id {item_id}")
|
|
215
|
+
item: TodoItem = db.read(item_id)
|
|
216
|
+
|
|
217
|
+
if _is_int(list_identifier):
|
|
218
|
+
new_list_: TodoList = db.read(list_identifier)
|
|
219
|
+
else:
|
|
220
|
+
new_list_: TodoList = Controller._find_model_by_stringmatch("name", list_identifier)
|
|
221
|
+
if not new_list_:
|
|
222
|
+
Controller.throw_error(f"Couldn't locate list by identifier {list_identifier}")
|
|
223
|
+
|
|
224
|
+
# update item
|
|
225
|
+
item.list_id = new_list_.id
|
|
226
|
+
db.update(item)
|
|
227
|
+
|
|
228
|
+
db.commit()
|
|
229
|
+
|
|
230
|
+
def add_dependency(src_id: int|str, dst_id: int|str):
|
|
231
|
+
"""Adds a depedency src -> dst, meaning src depends on dst"""
|
|
232
|
+
src: TodoItem = db.read(src_id)
|
|
233
|
+
src.dependency_ids.append(dst_id)
|
|
234
|
+
db.update(src)
|
|
235
|
+
db.commit()
|
|
236
|
+
|
|
184
237
|
# utilities
|
|
185
238
|
def _parse_remaining(remaining_args: list[str]) -> tuple[list, dict]:
|
|
186
239
|
|
|
@@ -211,40 +264,33 @@ def _is_int(val: any) -> bool:
|
|
|
211
264
|
except:
|
|
212
265
|
return False
|
|
213
266
|
|
|
214
|
-
def
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if not argv:
|
|
229
|
-
print(help_menu["general"])
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
if argv[0] in ("help","--help"):
|
|
233
|
-
print(help_menu["general"])
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
args, kwargs = _parse_remaining(argv)
|
|
237
|
-
GlobalConfig.load_dict(db.config)
|
|
238
|
-
|
|
267
|
+
def _clear_screen():
|
|
268
|
+
if sys.platform == "linux":
|
|
269
|
+
os.system("clear")
|
|
270
|
+
elif sys.platform == "windows":
|
|
271
|
+
os.system("cls")
|
|
272
|
+
else:
|
|
273
|
+
os.system("clear")
|
|
274
|
+
|
|
275
|
+
def execute_commands(*args, **kwargs) -> int:
|
|
276
|
+
"""
|
|
277
|
+
Return codes:
|
|
278
|
+
0 -> termination
|
|
279
|
+
1 -> continue
|
|
280
|
+
"""
|
|
239
281
|
try:
|
|
240
282
|
match args[0]:
|
|
283
|
+
case "help":
|
|
284
|
+
print(help_menu['general'])
|
|
285
|
+
|
|
241
286
|
case "setup":
|
|
242
287
|
Controller.run_setup_service()
|
|
243
288
|
|
|
244
289
|
case "show":
|
|
245
290
|
match args[1]:
|
|
246
291
|
case "all": Controller.show_all(*args[2:], **kwargs)
|
|
247
|
-
case "list": Controller.show_list(*args[2:], **kwargs)
|
|
292
|
+
case "list": Controller.show_list(*args[2:], **kwargs)
|
|
293
|
+
case "lists": Controller.show_lists()
|
|
248
294
|
case "item": Controller.show_item(*args[2:], **kwargs)
|
|
249
295
|
case "items": Controller.show_item(*args[2:], **kwargs)
|
|
250
296
|
case "examples": Controller.show_examples()
|
|
@@ -269,7 +315,7 @@ def entry_point():
|
|
|
269
315
|
case "item": Controller.delete(args[2])
|
|
270
316
|
case "list": Controller.delete(args[2])
|
|
271
317
|
case "completed" | "done": Controller.delete_completed()
|
|
272
|
-
case _: Controller.
|
|
318
|
+
case _: Controller.delete_list_by_name(args[1])
|
|
273
319
|
|
|
274
320
|
case "comment":
|
|
275
321
|
match args[1]:
|
|
@@ -302,10 +348,81 @@ def entry_point():
|
|
|
302
348
|
case "examples":
|
|
303
349
|
Controller.show_examples()
|
|
304
350
|
|
|
351
|
+
case "repair":
|
|
352
|
+
Controller.repair_service()
|
|
353
|
+
|
|
354
|
+
case "clear":
|
|
355
|
+
Controller.delete_completed()
|
|
356
|
+
|
|
357
|
+
case "move":
|
|
358
|
+
match args[1]:
|
|
359
|
+
case _ if _is_int(args[1]): Controller.move_item(args[1], args[2])
|
|
360
|
+
case _: Controller.throw_error("Unrecognized arguments", *args, **kwargs)
|
|
361
|
+
|
|
362
|
+
case "depend":
|
|
363
|
+
src_id, dst_id = args[1:3]
|
|
364
|
+
match (src_id, dst_id):
|
|
365
|
+
case _ if _is_int(src_id) and _is_int(dst_id): Controller.add_dependency(src_id, dst_id)
|
|
366
|
+
case _: Controller.throw_error("Invalid arrow argument, must be one of (->, <-)")
|
|
367
|
+
|
|
368
|
+
# developer use
|
|
369
|
+
case "db":
|
|
370
|
+
import orjson as json
|
|
371
|
+
print_json(json.dumps(db.read(args[1]).model_dump()).decode())
|
|
372
|
+
|
|
373
|
+
case "exit" | "exit()" | "quit":
|
|
374
|
+
return 0
|
|
375
|
+
|
|
305
376
|
case _: Controller.throw_error("unrecognized command", *args, **kwargs)
|
|
377
|
+
|
|
378
|
+
|
|
306
379
|
except Exception as e:
|
|
307
380
|
Controller.throw_error(f"encountered exception '{e}'", *args, **kwargs)
|
|
308
381
|
|
|
382
|
+
return 1
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def interactive_program():
|
|
386
|
+
# console = Console()
|
|
387
|
+
# builtins.print = console.print
|
|
388
|
+
|
|
389
|
+
response = ""
|
|
390
|
+
_clear_screen()
|
|
391
|
+
while True:
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
response = Prompt.ask("Type your commands:", default=response)
|
|
395
|
+
_clear_screen()
|
|
396
|
+
args, kwargs = _parse_remaining(response.split(" "))
|
|
397
|
+
if args[0] == "task":
|
|
398
|
+
args = args[1:]
|
|
399
|
+
return_code = execute_commands(*args, **kwargs)
|
|
400
|
+
if return_code == 0:
|
|
401
|
+
break
|
|
402
|
+
Console().rule()
|
|
403
|
+
|
|
404
|
+
except KeyboardInterrupt:
|
|
405
|
+
break
|
|
406
|
+
|
|
407
|
+
_clear_screen()
|
|
408
|
+
sys.exit(1)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def entry_point():
|
|
412
|
+
arg_parser = argparse.ArgumentParser()
|
|
413
|
+
_, argv = arg_parser.parse_known_args()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
if not argv:
|
|
417
|
+
interactive_program()
|
|
418
|
+
|
|
419
|
+
if argv[0] in ("help","--help"):
|
|
420
|
+
print(help_menu["general"])
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
args, kwargs = _parse_remaining(argv)
|
|
424
|
+
|
|
425
|
+
execute_commands(*args, **kwargs)
|
|
309
426
|
|
|
310
427
|
|
|
311
428
|
if __name__ == "__main__":
|
|
@@ -120,6 +120,21 @@ class JsonDirectoryDatabase:
|
|
|
120
120
|
return Comment(**self.comments[id_])
|
|
121
121
|
else:
|
|
122
122
|
raise RuntimeError("Can't find record to read")
|
|
123
|
+
|
|
124
|
+
def read_batch_attr(self, record_type: type[Base], attr: str, keep_nulls=False) -> dict[id, str]:
|
|
125
|
+
"""Returns a dictonary of attr values for a given attribute name"""
|
|
126
|
+
record_store = {
|
|
127
|
+
TodoItem: self.items,
|
|
128
|
+
TodoList: self.lists,
|
|
129
|
+
Comment: self.comments
|
|
130
|
+
}[record_type]
|
|
131
|
+
|
|
132
|
+
# TODO make sure attr is on the model type else throw error
|
|
133
|
+
|
|
134
|
+
records = {
|
|
135
|
+
id_: record[attr] for id_, record in record_store.items() if record.get(attr) or keep_nulls
|
|
136
|
+
}
|
|
137
|
+
return records
|
|
123
138
|
|
|
124
139
|
def update(self, record: Base) -> None:
|
|
125
140
|
|
|
@@ -128,17 +143,28 @@ class JsonDirectoryDatabase:
|
|
|
128
143
|
assert record.id in self.items, "item doesn't exist"
|
|
129
144
|
|
|
130
145
|
# update dependencies
|
|
131
|
-
old_item
|
|
146
|
+
old_item = self.items[record.id]
|
|
132
147
|
if old_item["list_id"] != record.list_id:
|
|
133
|
-
old_list
|
|
134
|
-
old_list
|
|
135
|
-
new_list
|
|
136
|
-
new_list
|
|
148
|
+
old_list = self.lists[old_item["list_id"]]
|
|
149
|
+
old_list["item_ids"].remove(record.id)
|
|
150
|
+
new_list = self.lists[record.list_id]
|
|
151
|
+
new_list["item_ids"].append(record.id)
|
|
137
152
|
|
|
138
153
|
self.items[record.id] = record.model_dump()
|
|
139
154
|
|
|
155
|
+
elif isinstance(record, TodoList):
|
|
156
|
+
assert record.id in self.lists, "list doesn't exist"
|
|
157
|
+
old_list: dict = self.lists[record.id]
|
|
158
|
+
for item_id in old_list["item_ids"]:
|
|
159
|
+
|
|
160
|
+
# Remove any items that have been dropped from the list that still hold it as their list id,
|
|
161
|
+
# as they are now orphans
|
|
162
|
+
if self.items[item_id]["list_id"] == old_list["id"] and item_id not in record.item_ids:
|
|
163
|
+
self.items.pop(item_id)
|
|
164
|
+
self.lists[record.id] = record.model_dump()
|
|
165
|
+
|
|
140
166
|
else:
|
|
141
|
-
raise RuntimeError("don't know what you're
|
|
167
|
+
raise RuntimeError("don't know what you're updating")
|
|
142
168
|
|
|
143
169
|
def delete(self, id_: int|str) -> None:
|
|
144
170
|
id_ = str(id_)
|
|
@@ -169,8 +195,10 @@ class JsonDirectoryDatabase:
|
|
|
169
195
|
def get_config_value(self, key: str) -> any:
|
|
170
196
|
return self.user_data["config"].get(key)
|
|
171
197
|
|
|
172
|
-
def get_config(self) ->
|
|
173
|
-
return self.user_data.get("config", {})
|
|
198
|
+
def get_config(self) -> CLIConfig:
|
|
199
|
+
return CLIConfig(**self.user_data.get("config", {}))
|
|
174
200
|
|
|
175
201
|
def set_config_value(self, key: str, value: any):
|
|
202
|
+
if key not in CLIConfig.model_fields:
|
|
203
|
+
raise RuntimeError("Invalid Config Option")
|
|
176
204
|
self.user_data["config"][key] = value
|
|
@@ -53,5 +53,11 @@ class Comment(Base):
|
|
|
53
53
|
user_id: Optional[str] = None
|
|
54
54
|
created_on: datetime = Field(default_factory=datetime.now)
|
|
55
55
|
|
|
56
|
+
class CLIConfig(Base):
|
|
57
|
+
|
|
58
|
+
# GEMINI
|
|
59
|
+
GEMINI_MODEL: str = None
|
|
60
|
+
GEMINI_API_KEY: str = None
|
|
61
|
+
|
|
56
62
|
class LLMConfig(Base):
|
|
57
63
|
pass
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# local
|
|
2
|
+
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
3
|
+
from taskai.models import TodoItem, TodoList
|
|
4
|
+
|
|
5
|
+
# external
|
|
6
|
+
from rich import print
|
|
7
|
+
|
|
8
|
+
def repair_database_service(
|
|
9
|
+
db: JsonDirectoryDatabase
|
|
10
|
+
):
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
Repairs the json database
|
|
14
|
+
"""
|
|
15
|
+
print(("Pruning null item references in lists ..."))
|
|
16
|
+
for list_id in db.lists:
|
|
17
|
+
list_: TodoList = db.read(list_id)
|
|
18
|
+
|
|
19
|
+
new_list_item_ids = []
|
|
20
|
+
for item_id in list_.item_ids:
|
|
21
|
+
if item_id not in db.items:
|
|
22
|
+
print(f"Pruned reference to null item id {item_id}")
|
|
23
|
+
else:
|
|
24
|
+
new_list_item_ids.append(item_id)
|
|
25
|
+
list_.item_ids = new_list_item_ids
|
|
26
|
+
db.update(list_)
|
|
27
|
+
|
|
28
|
+
print("Pruning orphan items")
|
|
29
|
+
bad_item_ids = set()
|
|
30
|
+
for item_id in db.items:
|
|
31
|
+
item: TodoItem = db.read(item_id)
|
|
32
|
+
if item.list_id not in db.lists:
|
|
33
|
+
print(f"Pruning orphan item {item_id}")
|
|
34
|
+
db.delete(bad_item_ids)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
db.commit()
|
|
38
|
+
print("Repair complete!")
|
|
39
|
+
|
|
@@ -8,7 +8,7 @@ from rich.console import Console
|
|
|
8
8
|
import rich
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def view_lists(db: JsonDirectoryDatabase, ids: list[str], show_done=False):
|
|
11
|
+
def view_lists(db: JsonDirectoryDatabase, ids: list[str], show_done=False, show_items=True):
|
|
12
12
|
"""Shows all the lists"""
|
|
13
13
|
|
|
14
14
|
|
|
@@ -25,18 +25,19 @@ def view_lists(db: JsonDirectoryDatabase, ids: list[str], show_done=False):
|
|
|
25
25
|
raise RuntimeError("cannot match list")
|
|
26
26
|
|
|
27
27
|
print(list.id, list.name)
|
|
28
|
-
|
|
28
|
+
if show_items:
|
|
29
|
+
for item_id in list.item_ids:
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
# silently repair lists with
|
|
32
|
+
if (item_id not in db.items):
|
|
33
|
+
list.item_ids.remove(item_id)
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
item: TodoItem = db.read(item_id)
|
|
37
|
+
if not item.completed:
|
|
38
|
+
print("\t",item.id, item.title)
|
|
39
|
+
elif show_done:
|
|
40
|
+
print(f"\t [strike]{item.id} {item.title}[/strike]")
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def view_item(db: JsonDirectoryDatabase, item: str|TodoItem):
|
|
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
|