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.
@@ -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
+ -
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskai-cli
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Author-email: Alex Paskal <alexcpaskal@gmail.com>
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: google-genai>=2.8
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "taskai-cli"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = ""
5
5
  authors = [{name = "Alex Paskal", email = "alexcpaskal@gmail.com"}]
6
6
  readme = "README.md"
@@ -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 _find_model_by_substring(attr: str, value: str) -> TodoItem|TodoList|Comment|None:
38
- for id_ in [*db.lists.keys(),*db.items.keys(),*db.comments.keys()]:
39
- model = db.read(id_)
40
- if (
41
- hasattr(model, attr) and
42
- isinstance(getattr(model, attr), str) and
43
- value in getattr(model, attr)
44
- ):
45
- return model
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=False):
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=False):
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=False):
65
- model = Controller._find_model_by_substring("name", value)
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 with substring '{value}'")
81
+ print(f"Could not find list matching pattern '{value}'")
70
82
 
71
- def show_list(list_id: int|str, show_done=False):
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._find_model_by_substring("name", list_id)
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, "%Y-%m-%d")
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, "%Y-%m-%d")
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 _matches_string(val: str, target: str) -> bool:
215
- # TODO make this follow linux rules
216
- if val in target:
217
- return True
218
- return False
219
-
220
-
221
-
222
- def entry_point():
223
-
224
- arg_parser = argparse.ArgumentParser()
225
- _, argv = arg_parser.parse_known_args()
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.throw_error("unrecognized delete command", *args, **kwargs)
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: TodoItem = self.items[record.id]
146
+ old_item = self.items[record.id]
132
147
  if old_item["list_id"] != record.list_id:
133
- old_list: TodoList = self.read(old_item["list_id"])
134
- old_list.item_ids.remove(record.id)
135
- new_list: TodoList = self.read(record.list_id)
136
- new_list.item_ids.append(record.id)
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 creating")
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) -> dict:
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
- for item_id in list.item_ids:
28
+ if show_items:
29
+ for item_id in list.item_ids:
29
30
 
30
- # silently repair lists with
31
- if (item_id not in db.items):
32
- list.item_ids.remove(item_id)
33
- continue
34
-
35
- item: TodoItem = db.read(item_id)
36
- if not item.completed:
37
- print("\t",item.id, item.title)
38
- elif show_done:
39
- print(f"\t [strike]{item.id} {item.title}[/strike]")
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