things.api
Module implementing the Things API.
The terms of the API aim to match those used in the Things app and Things URL Scheme. In some specific cases we instead choose to use terms that occur in the Things SQL database to closer reflect its underlying data structures. Whenever that happens, we define the new term here.
1""" 2Module implementing the Things API. 3 4The terms of the API aim to match those used in the Things app and 5Things URL Scheme. In some specific cases we instead choose to use terms 6that occur in the Things SQL database to closer reflect its underlying 7data structures. Whenever that happens, we define the new term here. 8""" 9 10import os 11from shlex import quote 12from typing import Dict, List, Union 13 14from things.database import Database 15 16 17# -------------------------------------------------- 18# Core functions 19# -------------------------------------------------- 20 21 22def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901 23 """ 24 Read tasks into dicts. 25 26 Note: "task" is a technical term used in the database to refer to a 27 to-do, project, or heading. For details, check the "type"-parameter. 28 29 Per default, only tasks marked as incomplete are included. If you 30 want to include completed or canceled tasks in the result, check the 31 "status"-parameter. 32 33 Parameters 34 ---------- 35 uuid : str or None, optional 36 Any valid task uuid. If None, then return all tasks matched. 37 38 include_items : bool, default False 39 Include items contained within a task. These might include 40 checklist items, headings, and to-dos. 41 42 type : {'to-do', 'heading', 'project', None}, optional 43 Only return a specific type of task: 44 - `'to-do'`: may have a checklist; may be in an area and have tags. 45 - `'project'`: may have to-dos and headings; may be in an area and 46 have tags. 47 - `'heading'`: part of a project; groups tasks. 48 - `None` (default): return all types of tasks. 49 50 status : {'incomplete', 'completed', 'canceled', None}, optional, \ 51 default 'incomplete' 52 53 Only include tasks matching that status. If `status == None`, 54 then include tasks with any status value. 55 56 start : {'Inbox', 'Anytime', 'Someday', None}, optional 57 Only include tasks matching that start value. If the argument is 58 `None` (default), then include tasks with any start value. 59 60 area : str or bool or None, optional 61 Any valid uuid of an area. Only include tasks matching that area. 62 Special cases: 63 - `area == False`, only include tasks _without_ an area. 64 - `area == True`, only include tasks _with_ an area. 65 - `area == None` (default), then include all tasks. 66 67 project : str or bool or None, optional 68 Any valid uuid of a project. Only include tasks matching that project. 69 Special cases: 70 - `project == False`, only include tasks _without_ a project. 71 - `project == True`, only include tasks _with_ a project. 72 - `project == None` (default), then include all tasks. 73 74 heading : str or None, optional 75 Any valid uuid of a heading. Only include tasks matching that heading. 76 Special cases: 77 - `heading == False`, only include tasks _without_ a heading. 78 - `heading == True`, only include tasks _with_ a heading. 79 - `heading == None` (default), then include all tasks. 80 81 tag : str or bool or None, optional 82 Any valid title of a tag. Only include tasks matching that tag. 83 Special cases: 84 - `tag == False`, only include tasks _without_ tags. 85 - `tag == True`, only include tasks _with_ tags. 86 - `tag == None` (default), then include all tasks. 87 88 start_date : bool, str or None, optional 89 - `start_date == False`, only include tasks _without_ a start date. 90 - `start_date == True`, only include tasks with a start date. 91 - `start_date == 'future'`, only include tasks with a future start date. 92 - `start_date == 'past'`, only include tasks with a past start date. 93 Note: this includes today's date. 94 - `start_date == '[operator]YYYY-MM-DD'`; match with optional operator, 95 for example: '2023-05-22', '<=2023-05-22', or '>2023-05-22'. 96 - `start_date == None` (default), then include all tasks. 97 98 stop_date : same options as start_date, signifies the date of completion 99 100 deadline : same options as start_date, signifies the deadline 101 102 deadline_suppressed : bool or None, optional 103 "deadline suppressed" is a technical term used in the database. 104 When tasks have an overdue deadline they show up in Today. 105 Some of us "suppress" some tasks, that is, move them out of 106 Today and into Inbox, Anytime, or Someday. For those moved tasks 107 the deadline is said to be suppressed. 108 - `deadline_suppressed == True`, only include tasks with deadlines 109 and whose deadlines have been suppressed. 110 - `deadline_suppressed == False`, include any task _except_ those 111 with suppressed deadlines. 112 - `deadline_suppressed == None` (default), include any task. 113 114 trashed : bool or None, optional, default False 115 - `trashed == False` (default), only include non-trashed tasks. 116 - `trashed == True`, only include trashed tasks. 117 - `trashed == None`, include both kind of tasks. 118 119 context_trashed : bool or None, optional, default False 120 Some tasks may be within a context of a project or a heading. 121 This manages how to handle such tasks. The other tasks are not 122 affected by this setting. 123 124 - `context_trashed == False` (default), for tasks within a context, 125 only return tasks whose context has _not_ been trashed. 126 - `context_trashed == True`, for tasks within a context, 127 only return tasks whose context has been trashed. 128 - `context_trashed == None`, include both kind of tasks. 129 130 last : str, optional 131 Limit returned tasks to tasks created within the last X days, 132 weeks, or years. For example: '3d', '5w', or '1y'. 133 134 Takes as input an offset string of the format 'X[d/w/y]' where X 135 is a non-negative integer that is followed by 'd', 'w', or 'y' 136 which stand for 'days', 'weeks', and 'years', respectively. 137 138 search_query : str, optional 139 The string value is passed to the SQL LIKE operator. It can thus 140 include placeholders such as '%' and '_'. Per default, it is 141 wrapped in '% ... %'. 142 143 Currently titles and notes of to-dos, projects, headings, and areas 144 are taken into account. 145 146 index : {'index', 'todayIndex'}, default 'index' 147 Database field to order result by. 148 149 count_only : bool, default False 150 Only output length of result. This is done by a SQL COUNT query. 151 152 print_sql : bool, default False 153 Print every SQL query performed. Some may contain '?' and ':' 154 characters, which correspond to SQLite parameter tokens. 155 See https://www.sqlite.org/lang_expr.html#varparam 156 157 filepath : str, optional 158 Any valid path of a SQLite database file generated by the Things app. 159 If the environment variable `THINGSDB` is set, then use that path. 160 Otherwise, access the default database path. 161 162 database : things.database.Database, optional 163 Any valid database object previously instantiated. 164 165 Returns 166 ------- 167 list of dict (default) 168 Representing multiple tasks. 169 dict (if `uuid` is given) 170 Representing a single task. 171 int (`count_only == True`) 172 Count of matching Tasks. 173 174 Examples 175 -------- 176 >>> things.tasks() 177 [{'uuid': '6Hf2qWBjWhq7B1xszwdo34', 'type': 'to-do', 'title':... 178 >>> things.tasks('DfYoiXcNLQssk9DkSoJV3Y') 179 {'uuid': 'DfYoiXcNLQssk9DkSoJV3Y', 'type': 'to-do', 'title': ... 180 >>> things.tasks(area='hIo1FJlAYGKt1Yj38vzKc3', include_items=True) 181 [] 182 >>> things.tasks(status='completed', count_only=True) 183 10 184 >>> things.tasks(status='completed', last='1w', count_only=True) 185 0 186 187 """ 188 database = pop_database(kwargs) 189 result = database.get_tasks( 190 uuid=uuid, status=kwargs.pop("status", "incomplete"), **kwargs 191 ) 192 193 if kwargs.get("count_only"): 194 return result 195 196 # overwrite `include_items` if fetching a single task. 197 if uuid: 198 include_items = True 199 200 for task in result: 201 # TK: How costly of an operation is it to do this for every task? 202 # IF costly, then can it be made significantly more efficient 203 # by optimizing SQL calls? 204 205 if task.get("tags"): 206 task["tags"] = database.get_tags(task=task["uuid"]) 207 208 if not include_items: 209 continue 210 211 # include items 212 if task["type"] == "to-do": 213 if task.get("checklist"): 214 task["checklist"] = database.get_checklist_items(task["uuid"]) 215 elif task["type"] == "project": 216 project = task 217 project["items"] = items = tasks( 218 project=project["uuid"], 219 context_trashed=None, 220 include_items=True, 221 database=database, 222 ) 223 # to-dos without headings appear before headings in app 224 items.sort(key=lambda item: item["type"], reverse=True) 225 elif task["type"] == "heading": 226 heading = task 227 heading["items"] = tasks( 228 type="to-do", 229 heading=heading["uuid"], 230 context_trashed=None, 231 include_items=True, 232 database=database, 233 ) 234 235 if uuid: 236 result = result[0] 237 238 return result 239 240 241def areas(uuid=None, include_items=False, **kwargs): 242 """ 243 Read areas into dicts. 244 245 Parameters 246 ---------- 247 uuid : str or None, optional 248 Any valid uuid of an area. If None, then return all areas. 249 250 include_items : bool, default False 251 Include tasks and projects in each area. 252 253 tag : str or bool or None, optional 254 Any valid title of a tag. Only include areas matching that tag. 255 Special cases: 256 - `tag == False`, only include areas _without_ tags. 257 - `tag == True`, only include areas _with_ tags. 258 - `tag == None`, then ignore any tags present, that is, 259 include areas both with and without tags. 260 261 count_only : bool, default False 262 Only output length of result. This is done by a SQL COUNT query. 263 264 filepath : str, optional 265 Any valid path of a SQLite database file generated by the Things app. 266 If no path is provided, then access the default database path. 267 268 database : things.database.Database, optional 269 Any valid `things.database.Database` object previously instantiated. 270 271 Returns 272 ------- 273 list of dict (default) 274 Representing Things areas. 275 dict (if `uuid` is given) 276 Representing a single Things area. 277 int (`count_only == True`) 278 Count of matching areas. 279 280 Examples 281 -------- 282 >>> things.areas() 283 [{'uuid': 'Y3JC4XeyGWxzDocQL4aobo', 'type': 'area', 'title': 'Area 3'}, ... 284 >>> things.areas(tag='Home') 285 [] 286 >>> things.areas(uuid='DciSFacytdrNG1nRaMJPgY') 287 {'uuid': 'DciSFacytdrNG1nRaMJPgY', 'type': 'area', 'title': 'Area 1', ... 288 >>> things.areas(include_items=True, tag='Errand') 289 [{'uuid': 'DciSFacytdrNG1nRaMJPgY', 'type': 'area', 'title': 'Area 1', ... 290 """ 291 database = pop_database(kwargs) 292 result = database.get_areas(uuid=uuid, **kwargs) 293 294 if kwargs.get("count_only"): 295 return result 296 297 for area in result: 298 if area.get("tags"): 299 area["tags"] = database.get_tags(area=area["uuid"]) 300 if include_items: 301 area["items"] = tasks( 302 area=area["uuid"], include_items=True, database=database 303 ) 304 305 if uuid: 306 result = result[0] 307 308 return result 309 310 311def tags(title=None, include_items=False, **kwargs): 312 """ 313 Read tags into dicts. 314 315 Parameters 316 ---------- 317 title : str, optional 318 Any valid title of a tag. Include all items of said tag. 319 If None, then return all tags. 320 321 include_items : bool, default False 322 For each tag, include items tagged with that tag. 323 Items may include areas, tasks, and projects. 324 325 area : str, optional 326 Valid uuid of an area. Return tags of said area. 327 328 task : str, optional 329 Valid uuid of a task. Return tags of said task. 330 331 titles_only : bool, default False 332 If True, only return list of titles of tags. 333 334 filepath : str, optional 335 Any valid path of a SQLite database file generated by the Things app. 336 If no path is provided, then access the default database path. 337 338 database : things.database.Database, optional 339 Any valid database object previously instantiated. 340 341 Returns 342 ------- 343 list of dict (default) 344 Representing tags. 345 list of str (if `titles_only == True` or area / task is given) 346 Representing tag titles. 347 dict (if `title` is given) 348 Representing a single Things tag. 349 350 Examples 351 -------- 352 >>> things.tags() 353 [{'uuid': 'H96sVJwE7VJveAnv7itmux', 'type': 'tag', 'title': 'Errand', ... 354 >>> things.tags('Home') 355 {'uuid': 'CK9dARrf2ezbFvrVUUxkHE', 'type': 'tag', 'title': 'Home', ... 356 >>> things.tags(include_items=True) 357 [{'uuid': 'H96sVJwE7VJveAnv7itmux', 'type': 'tag', 'title': 'Errand', ... 358 >>> things.tags(task='2Ukg8I2nLukhyEM7wYiBeb') 359 [] 360 """ 361 database = pop_database(kwargs) 362 result = database.get_tags(title=title, **kwargs) 363 364 if include_items: 365 for tag in result: 366 tag_title = tag["title"] 367 tag["items"] = [ 368 *areas(tag=tag_title, database=database), 369 *tasks(tag=tag_title, database=database), 370 ] 371 372 if title: 373 result = result[0] 374 375 return result 376 377 378def checklist_items(todo_uuid, **kwargs): 379 """ 380 Read checklist items of to-dos into dicts. 381 382 Note: checklists are contained in the return value of 383 `things.todos(todo_uuid)` and `things.tasks(todo_uuid)`. 384 385 Parameters 386 ---------- 387 todo_uuid : str, optional 388 A valid to-do uuid. 389 390 Returns 391 ------- 392 list of dict 393 Checklist items. 394 """ 395 database = pop_database(kwargs) 396 return database.get_checklist_items(todo_uuid=todo_uuid) 397 398 399# -------------------------------------------------- 400# Utility API functions derived from above 401# -------------------------------------------------- 402 403 404def search(query: str, **kwargs) -> List[Dict]: 405 """ 406 Search tasks in the database. 407 408 Currently any part of a title and note of a to-do, project, 409 heading, or area is matched. 410 411 See the `search_query` parameter of `things.api.tasks` for details. 412 413 Examples 414 -------- 415 >>> things.search('Today%yellow') 416 [{'uuid': '6Hf2qWBjWhq7B1xszwdo34', 417 'type': 'to-do', 418 'title': 'Upcoming To-Do in Today (yellow)', 419 'status': 'incomplete', 420 'notes': '', 421 ... 422 """ 423 return tasks(search_query=query, **kwargs) 424 425 426def get(uuid, default=None, **kwargs): 427 """ 428 Find an object by uuid. If not found, return `default`. 429 430 Currently supports tasks, projects, headings, areas, and tags. 431 """ 432 try: 433 return tasks(uuid=uuid, **kwargs) 434 except ValueError: 435 pass 436 437 try: 438 return areas(uuid=uuid, **kwargs) 439 except ValueError: 440 pass 441 442 for tag in tags(**kwargs): 443 if tag["uuid"] == uuid: 444 return tag 445 446 return default 447 448 449# Filter by object type 450 451 452def todos(uuid=None, **kwargs): 453 """ 454 Read to-dos into dicts. 455 456 See `things.api.tasks` for details on the optional parameters. 457 """ 458 return tasks(uuid=uuid, type="to-do", **kwargs) 459 460 461def projects(uuid=None, **kwargs): 462 """ 463 Read projects into dicts. 464 465 See `things.api.tasks` for details on the optional parameters. 466 """ 467 return tasks(uuid=uuid, type="project", **kwargs) 468 469 470# Filter by collections in the Things app sidebar. 471 472 473def inbox(**kwargs): 474 """ 475 Read Inbox into dicts. 476 477 See `things.api.tasks` for details on the optional parameters. 478 """ 479 return tasks(start="Inbox", **kwargs) 480 481 482def today(**kwargs): 483 """ 484 Read Today's tasks into dicts. 485 486 Note: The Things database reflects the state of the Things app when 487 it was last opened. For the Today tasks that means the database 488 might not be up to date anymore if you didn't open the app recently. 489 To get around this limitation, we here make a prediction of what 490 tasks would show up in Today if you were to open the app right now. 491 This prediction does not include repeating tasks at this time. 492 493 See `things.api.tasks` for details on the optional parameters. 494 """ 495 database = pop_database(kwargs) 496 kwargs["database"] = database 497 498 regular_today_tasks = tasks( 499 start_date=True, 500 start="Anytime", 501 index="todayIndex", 502 **kwargs, 503 ) 504 505 # Predictions of new to-dos indicated by a yellow dot in the app. 506 507 unconfirmed_scheduled_tasks = tasks( 508 start_date="past", 509 start="Someday", 510 index="todayIndex", 511 **kwargs, 512 ) 513 514 unconfirmed_overdue_tasks = tasks( 515 start_date=False, 516 deadline="past", 517 deadline_suppressed=False, 518 **kwargs, 519 ) 520 521 result = [ 522 *regular_today_tasks, 523 *unconfirmed_scheduled_tasks, 524 *unconfirmed_overdue_tasks, 525 ] 526 result.sort(key=lambda task: (task["today_index"], task["start_date"])) 527 528 return result 529 530 531def upcoming(**kwargs): 532 """ 533 Read Upcoming tasks into dicts. 534 535 Note: unscheduled tasks with a deadline are not included here. 536 See the `things.api.deadline` function instead. 537 538 For details on parameters, see `things.api.tasks`. 539 """ 540 return tasks(start_date="future", start="Someday", **kwargs) 541 542 543def anytime(**kwargs): 544 """ 545 Read Anytime tasks into dicts. 546 547 See `things.api.tasks` for details on the optional parameters. 548 """ 549 return tasks(start="Anytime", **kwargs) 550 551 552def someday(**kwargs): 553 """ 554 Read Someday tasks into dicts. 555 556 See `things.api.tasks` for details on the optional parameters. 557 """ 558 return tasks(start_date=False, start="Someday", **kwargs) 559 560 561def logbook(**kwargs): 562 """ 563 Read Logbook tasks into dicts. 564 565 See `things.api.tasks` for details on the optional parameters. 566 """ 567 result = [*canceled(**kwargs), *completed(**kwargs)] 568 result.sort(key=lambda task: task["stop_date"], reverse=True) 569 return result 570 571 572def trash(**kwargs): 573 """ 574 Read Trash tasks into dicts. 575 576 See `things.api.tasks` for details on the optional parameters. 577 """ 578 return tasks( 579 trashed=True, 580 context_trashed=kwargs.pop("context_trashed", None), 581 status=kwargs.pop("status", None), 582 **kwargs, 583 ) 584 585 586# Filter by various task properties 587 588 589def canceled(**kwargs): 590 """ 591 Read canceled tasks into dicts. 592 593 See `things.api.tasks` for details on the optional parameters. 594 """ 595 return tasks(status="canceled", **kwargs) 596 597 598def completed(**kwargs): 599 """ 600 Read completed tasks into dicts. 601 602 See `things.api.tasks` for details on the optional parameters. 603 604 Examples 605 -------- 606 >>> things.completed(count_only=True) 607 10 608 >>> things.completed(type='project', count_only=True) 609 0 610 >>> things.completed(type='to-do', last='1w') 611 [] 612 """ 613 return tasks(status="completed", **kwargs) 614 615 616def deadlines(**kwargs): 617 """ 618 Read tasks with deadlines into dicts. 619 620 See `things.api.tasks` for details on the optional parameters. 621 """ 622 result = tasks(deadline=True, **kwargs) 623 result.sort(key=lambda task: task["deadline"]) 624 return result 625 626 627def last(offset, **kwargs): 628 """ 629 Read tasks created within last X days, weeks, or years into dicts. 630 631 Per default, only incomplete tasks are included, but do see 632 `things.api.tasks` for details on the optional parameters. 633 634 Parameters 635 ---------- 636 offset : str 637 A valid date offset such as '3d', '5w', or '1y'. 638 For details, see the `last` parameter of `things.api.tasks`. 639 640 Returns 641 ------- 642 list of dict 643 Tasks within offset ordered by creation date. Newest first. 644 645 Examples 646 -------- 647 >>> things.last('3d') 648 [] 649 >>> things.last('1w', status='completed') 650 [] 651 >>> things.last('1y', tag='Important', status='completed', count_only=True) 652 0 653 654 """ 655 if offset is None: 656 raise ValueError(f"Invalid offset type: {offset!r}") 657 658 result = tasks(last=offset, **kwargs) 659 if result: 660 result.sort(key=lambda task: task["created"], reverse=True) 661 662 return result 663 664 665# Interact with Things app 666 667 668def token(**kwargs) -> Union[str, None]: 669 """ 670 Read the Things URL scheme authentication token. 671 672 You can make good use of this token to modify existing Things data 673 using the Things URL Scheme. For details, see 674 [here](https://culturedcode.com/things/help/url-scheme/). 675 """ 676 database = pop_database(kwargs) 677 return database.get_url_scheme_auth_token() 678 679 680def link(uuid): 681 """Return a things:///show?id=uuid link.""" 682 return f"things:///show?id={uuid}" 683 684 685def show(uuid): # noqa 686 """ 687 Show a certain uuid in the Things app. 688 689 Parameters 690 ---------- 691 uuid : str 692 A valid uuid of any Things object. 693 694 Examples 695 -------- 696 >>> tag = things.tags('Home') 697 >>> things.show(tag['uuid']) # doctest: +SKIP 698 """ 699 uri = link(uuid) 700 os.system(f"open {quote(uri)}") 701 702 703# Helper functions 704 705 706def pop_database(kwargs): 707 """Instantiate non-default database from `kwargs` if provided.""" 708 filepath = kwargs.pop("filepath", None) 709 database = kwargs.pop("database", None) 710 print_sql = kwargs.pop("print_sql", False) 711 712 if not database: 713 database = Database(filepath=filepath, print_sql=print_sql) 714 return database
23def tasks(uuid=None, include_items=False, **kwargs): # noqa: C901 24 """ 25 Read tasks into dicts. 26 27 Note: "task" is a technical term used in the database to refer to a 28 to-do, project, or heading. For details, check the "type"-parameter. 29 30 Per default, only tasks marked as incomplete are included. If you 31 want to include completed or canceled tasks in the result, check the 32 "status"-parameter. 33 34 Parameters 35 ---------- 36 uuid : str or None, optional 37 Any valid task uuid. If None, then return all tasks matched. 38 39 include_items : bool, default False 40 Include items contained within a task. These might include 41 checklist items, headings, and to-dos. 42 43 type : {'to-do', 'heading', 'project', None}, optional 44 Only return a specific type of task: 45 - `'to-do'`: may have a checklist; may be in an area and have tags. 46 - `'project'`: may have to-dos and headings; may be in an area and 47 have tags. 48 - `'heading'`: part of a project; groups tasks. 49 - `None` (default): return all types of tasks. 50 51 status : {'incomplete', 'completed', 'canceled', None}, optional, \ 52 default 'incomplete' 53 54 Only include tasks matching that status. If `status == None`, 55 then include tasks with any status value. 56 57 start : {'Inbox', 'Anytime', 'Someday', None}, optional 58 Only include tasks matching that start value. If the argument is 59 `None` (default), then include tasks with any start value. 60 61 area : str or bool or None, optional 62 Any valid uuid of an area. Only include tasks matching that area. 63 Special cases: 64 - `area == False`, only include tasks _without_ an area. 65 - `area == True`, only include tasks _with_ an area. 66 - `area == None` (default), then include all tasks. 67 68 project : str or bool or None, optional 69 Any valid uuid of a project. Only include tasks matching that project. 70 Special cases: 71 - `project == False`, only include tasks _without_ a project. 72 - `project == True`, only include tasks _with_ a project. 73 - `project == None` (default), then include all tasks. 74 75 heading : str or None, optional 76 Any valid uuid of a heading. Only include tasks matching that heading. 77 Special cases: 78 - `heading == False`, only include tasks _without_ a heading. 79 - `heading == True`, only include tasks _with_ a heading. 80 - `heading == None` (default), then include all tasks. 81 82 tag : str or bool or None, optional 83 Any valid title of a tag. Only include tasks matching that tag. 84 Special cases: 85 - `tag == False`, only include tasks _without_ tags. 86 - `tag == True`, only include tasks _with_ tags. 87 - `tag == None` (default), then include all tasks. 88 89 start_date : bool, str or None, optional 90 - `start_date == False`, only include tasks _without_ a start date. 91 - `start_date == True`, only include tasks with a start date. 92 - `start_date == 'future'`, only include tasks with a future start date. 93 - `start_date == 'past'`, only include tasks with a past start date. 94 Note: this includes today's date. 95 - `start_date == '[operator]YYYY-MM-DD'`; match with optional operator, 96 for example: '2023-05-22', '<=2023-05-22', or '>2023-05-22'. 97 - `start_date == None` (default), then include all tasks. 98 99 stop_date : same options as start_date, signifies the date of completion 100 101 deadline : same options as start_date, signifies the deadline 102 103 deadline_suppressed : bool or None, optional 104 "deadline suppressed" is a technical term used in the database. 105 When tasks have an overdue deadline they show up in Today. 106 Some of us "suppress" some tasks, that is, move them out of 107 Today and into Inbox, Anytime, or Someday. For those moved tasks 108 the deadline is said to be suppressed. 109 - `deadline_suppressed == True`, only include tasks with deadlines 110 and whose deadlines have been suppressed. 111 - `deadline_suppressed == False`, include any task _except_ those 112 with suppressed deadlines. 113 - `deadline_suppressed == None` (default), include any task. 114 115 trashed : bool or None, optional, default False 116 - `trashed == False` (default), only include non-trashed tasks. 117 - `trashed == True`, only include trashed tasks. 118 - `trashed == None`, include both kind of tasks. 119 120 context_trashed : bool or None, optional, default False 121 Some tasks may be within a context of a project or a heading. 122 This manages how to handle such tasks. The other tasks are not 123 affected by this setting. 124 125 - `context_trashed == False` (default), for tasks within a context, 126 only return tasks whose context has _not_ been trashed. 127 - `context_trashed == True`, for tasks within a context, 128 only return tasks whose context has been trashed. 129 - `context_trashed == None`, include both kind of tasks. 130 131 last : str, optional 132 Limit returned tasks to tasks created within the last X days, 133 weeks, or years. For example: '3d', '5w', or '1y'. 134 135 Takes as input an offset string of the format 'X[d/w/y]' where X 136 is a non-negative integer that is followed by 'd', 'w', or 'y' 137 which stand for 'days', 'weeks', and 'years', respectively. 138 139 search_query : str, optional 140 The string value is passed to the SQL LIKE operator. It can thus 141 include placeholders such as '%' and '_'. Per default, it is 142 wrapped in '% ... %'. 143 144 Currently titles and notes of to-dos, projects, headings, and areas 145 are taken into account. 146 147 index : {'index', 'todayIndex'}, default 'index' 148 Database field to order result by. 149 150 count_only : bool, default False 151 Only output length of result. This is done by a SQL COUNT query. 152 153 print_sql : bool, default False 154 Print every SQL query performed. Some may contain '?' and ':' 155 characters, which correspond to SQLite parameter tokens. 156 See https://www.sqlite.org/lang_expr.html#varparam 157 158 filepath : str, optional 159 Any valid path of a SQLite database file generated by the Things app. 160 If the environment variable `THINGSDB` is set, then use that path. 161 Otherwise, access the default database path. 162 163 database : things.database.Database, optional 164 Any valid database object previously instantiated. 165 166 Returns 167 ------- 168 list of dict (default) 169 Representing multiple tasks. 170 dict (if `uuid` is given) 171 Representing a single task. 172 int (`count_only == True`) 173 Count of matching Tasks. 174 175 Examples 176 -------- 177 >>> things.tasks() 178 [{'uuid': '6Hf2qWBjWhq7B1xszwdo34', 'type': 'to-do', 'title':... 179 >>> things.tasks('DfYoiXcNLQssk9DkSoJV3Y') 180 {'uuid': 'DfYoiXcNLQssk9DkSoJV3Y', 'type': 'to-do', 'title': ... 181 >>> things.tasks(area='hIo1FJlAYGKt1Yj38vzKc3', include_items=True) 182 [] 183 >>> things.tasks(status='completed', count_only=True) 184 10 185 >>> things.tasks(status='completed', last='1w', count_only=True) 186 0 187 188 """ 189 database = pop_database(kwargs) 190 result = database.get_tasks( 191 uuid=uuid, status=kwargs.pop("status", "incomplete"), **kwargs 192 ) 193 194 if kwargs.get("count_only"): 195 return result 196 197 # overwrite `include_items` if fetching a single task. 198 if uuid: 199 include_items = True 200 201 for task in result: 202 # TK: How costly of an operation is it to do this for every task? 203 # IF costly, then can it be made significantly more efficient 204 # by optimizing SQL calls? 205 206 if task.get("tags"): 207 task["tags"] = database.get_tags(task=task["uuid"]) 208 209 if not include_items: 210 continue 211 212 # include items 213 if task["type"] == "to-do": 214 if task.get("checklist"): 215 task["checklist"] = database.get_checklist_items(task["uuid"]) 216 elif task["type"] == "project": 217 project = task 218 project["items"] = items = tasks( 219 project=project["uuid"], 220 context_trashed=None, 221 include_items=True, 222 database=database, 223 ) 224 # to-dos without headings appear before headings in app 225 items.sort(key=lambda item: item["type"], reverse=True) 226 elif task["type"] == "heading": 227 heading = task 228 heading["items"] = tasks( 229 type="to-do", 230 heading=heading["uuid"], 231 context_trashed=None, 232 include_items=True, 233 database=database, 234 ) 235 236 if uuid: 237 result = result[0] 238 239 return result
Read tasks into dicts.
Note: "task" is a technical term used in the database to refer to a to-do, project, or heading. For details, check the "type"-parameter.
Per default, only tasks marked as incomplete are included. If you want to include completed or canceled tasks in the result, check the "status"-parameter.
Parameters
- uuid (str or None, optional): Any valid task uuid. If None, then return all tasks matched.
- include_items (bool, default False): Include items contained within a task. These might include checklist items, headings, and to-dos.
- type ({'to-do', 'heading', 'project', None}, optional):
Only return a specific type of task:
'to-do'
: may have a checklist; may be in an area and have tags.'project'
: may have to-dos and headings; may be in an area and have tags.'heading'
: part of a project; groups tasks.None
(default): return all types of tasks.
- status ({'incomplete', 'completed', 'canceled', None}, optional, default 'incomplete'):
Only include tasks matching that status. If
status == None
, then include tasks with any status value. - start ({'Inbox', 'Anytime', 'Someday', None}, optional):
Only include tasks matching that start value. If the argument is
None
(default), then include tasks with any start value. - area (str or bool or None, optional):
Any valid uuid of an area. Only include tasks matching that area.
Special cases:
area == False
, only include tasks _without_ an area.area == True
, only include tasks _with_ an area.area == None
(default), then include all tasks.
- project (str or bool or None, optional):
Any valid uuid of a project. Only include tasks matching that project.
Special cases:
project == False
, only include tasks _without_ a project.project == True
, only include tasks _with_ a project.project == None
(default), then include all tasks.
- heading (str or None, optional):
Any valid uuid of a heading. Only include tasks matching that heading.
Special cases:
heading == False
, only include tasks _without_ a heading.heading == True
, only include tasks _with_ a heading.heading == None
(default), then include all tasks.
- tag (str or bool or None, optional):
Any valid title of a tag. Only include tasks matching that tag.
Special cases:
tag == False
, only include tasks _without_ tags.tag == True
, only include tasks _with_ tags.tag == None
(default), then include all tasks.
- start_date (bool, str or None, optional):
start_date == False
, only include tasks _without_ a start date.start_date == True
, only include tasks with a start date.start_date == 'future'
, only include tasks with a future start date.start_date == 'past'
, only include tasks with a past start date. Note: this includes today's date.start_date == '[operator]YYYY-MM-DD'
; match with optional operator, for example: '2023-05-22', '<=2023-05-22', or '>2023-05-22'.start_date == None
(default), then include all tasks.
stop_date (same options as start_date, signifies the date of completion):
deadline (same options as start_date, signifies the deadline):
deadline_suppressed (bool or None, optional): "deadline suppressed" is a technical term used in the database. When tasks have an overdue deadline they show up in Today. Some of us "suppress" some tasks, that is, move them out of Today and into Inbox, Anytime, or Someday. For those moved tasks the deadline is said to be suppressed.
deadline_suppressed == True
, only include tasks with deadlines and whose deadlines have been suppressed.deadline_suppressed == False
, include any task _except_ those with suppressed deadlines.deadline_suppressed == None
(default), include any task.
- trashed (bool or None, optional, default False):
trashed == False
(default), only include non-trashed tasks.trashed == True
, only include trashed tasks.trashed == None
, include both kind of tasks.
context_trashed (bool or None, optional, default False): Some tasks may be within a context of a project or a heading. This manages how to handle such tasks. The other tasks are not affected by this setting.
context_trashed == False
(default), for tasks within a context, only return tasks whose context has _not_ been trashed.context_trashed == True
, for tasks within a context, only return tasks whose context has been trashed.context_trashed == None
, include both kind of tasks.
last (str, optional): Limit returned tasks to tasks created within the last X days, weeks, or years. For example: '3d', '5w', or '1y'.
Takes as input an offset string of the format 'X[d/w/y]' where X is a non-negative integer that is followed by 'd', 'w', or 'y' which stand for 'days', 'weeks', and 'years', respectively.
search_query (str, optional): The string value is passed to the SQL LIKE operator. It can thus include placeholders such as '%' and '_'. Per default, it is wrapped in '% ... %'.
Currently titles and notes of to-dos, projects, headings, and areas are taken into account.
- index ({'index', 'todayIndex'}, default 'index'): Database field to order result by.
- count_only (bool, default False): Only output length of result. This is done by a SQL COUNT query.
- print_sql (bool, default False): Print every SQL query performed. Some may contain '?' and ':' characters, which correspond to SQLite parameter tokens. See https://www.sqlite.org/lang_expr.html#varparam
- filepath (str, optional):
Any valid path of a SQLite database file generated by the Things app.
If the environment variable
THINGSDB
is set, then use that path. Otherwise, access the default database path. - database (things.database.Database, optional): Any valid database object previously instantiated.
Returns
- list of dict (default): Representing multiple tasks.
- dict (if
uuid
is given): Representing a single task. - int (
count_only == True
): Count of matching Tasks.
Examples
>>> things.tasks()
[{'uuid': '6Hf2qWBjWhq7B1xszwdo34', 'type': 'to-do', 'title':...
>>> things.tasks('DfYoiXcNLQssk9DkSoJV3Y')
{'uuid': 'DfYoiXcNLQssk9DkSoJV3Y', 'type': 'to-do', 'title': ...
>>> things.tasks(area='hIo1FJlAYGKt1Yj38vzKc3', include_items=True)
[]
>>> things.tasks(status='completed', count_only=True)
10
>>> things.tasks(status='completed', last='1w', count_only=True)
0
242def areas(uuid=None, include_items=False, **kwargs): 243 """ 244 Read areas into dicts. 245 246 Parameters 247 ---------- 248 uuid : str or None, optional 249 Any valid uuid of an area. If None, then return all areas. 250 251 include_items : bool, default False 252 Include tasks and projects in each area. 253 254 tag : str or bool or None, optional 255 Any valid title of a tag. Only include areas matching that tag. 256 Special cases: 257 - `tag == False`, only include areas _without_ tags. 258 - `tag == True`, only include areas _with_ tags. 259 - `tag == None`, then ignore any tags present, that is, 260 include areas both with and without tags. 261 262 count_only : bool, default False 263 Only output length of result. This is done by a SQL COUNT query. 264 265 filepath : str, optional 266 Any valid path of a SQLite database file generated by the Things app. 267 If no path is provided, then access the default database path. 268 269 database : things.database.Database, optional 270 Any valid `things.database.Database` object previously instantiated. 271 272 Returns 273 ------- 274 list of dict (default) 275 Representing Things areas. 276 dict (if `uuid` is given) 277 Representing a single Things area. 278 int (`count_only == True`) 279 Count of matching areas. 280 281 Examples 282 -------- 283 >>> things.areas() 284 [{'uuid': 'Y3JC4XeyGWxzDocQL4aobo', 'type': 'area', 'title': 'Area 3'}, ... 285 >>> things.areas(tag='Home') 286 [] 287 >>> things.areas(uuid='DciSFacytdrNG1nRaMJPgY') 288 {'uuid': 'DciSFacytdrNG1nRaMJPgY', 'type': 'area', 'title': 'Area 1', ... 289 >>> things.areas(include_items=True, tag='Errand') 290 [{'uuid': 'DciSFacytdrNG1nRaMJPgY', 'type': 'area', 'title': 'Area 1', ... 291 """ 292 database = pop_database(kwargs) 293 result = database.get_areas(uuid=uuid, **kwargs) 294 295 if kwargs.get("count_only"): 296 return result 297 298 for area in result: 299 if area.get("tags"): 300 area["tags"] = database.get_tags(area=area["uuid"]) 301 if include_items: 302 area["items"] = tasks( 303 area=area["uuid"], include_items=True, database=database 304 ) 305 306 if uuid: 307 result = result[0] 308 309 return result
Read areas into dicts.
Parameters
- uuid (str or None, optional): Any valid uuid of an area. If None, then return all areas.
- include_items (bool, default False): Include tasks and projects in each area.
- tag (str or bool or None, optional):
Any valid title of a tag. Only include areas matching that tag.
Special cases:
tag == False
, only include areas _without_ tags.tag == True
, only include areas _with_ tags.tag == None
, then ignore any tags present, that is, include areas both with and without tags.
- count_only (bool, default False): Only output length of result. This is done by a SQL COUNT query.
- filepath (str, optional): Any valid path of a SQLite database file generated by the Things app. If no path is provided, then access the default database path.
- database (things.database.Database, optional):
Any valid
things.database.Database
object previously instantiated.
Returns
- list of dict (default): Representing Things areas.
- dict (if
uuid
is given): Representing a single Things area. - int (
count_only == True
): Count of matching areas.
Examples
>>> things.areas()
[{'uuid': 'Y3JC4XeyGWxzDocQL4aobo', 'type': 'area', 'title': 'Area 3'}, ...
>>> things.areas(tag='Home')
[]
>>> things.areas(uuid='DciSFacytdrNG1nRaMJPgY')
{'uuid': 'DciSFacytdrNG1nRaMJPgY', 'type': 'area', 'title': 'Area 1', ...
>>> things.areas(include_items=True, tag='Errand')
[{'uuid': 'DciSFacytdrNG1nRaMJPgY', 'type': 'area', 'title': 'Area 1', ...
379def checklist_items(todo_uuid, **kwargs): 380 """ 381 Read checklist items of to-dos into dicts. 382 383 Note: checklists are contained in the return value of 384 `things.todos(todo_uuid)` and `things.tasks(todo_uuid)`. 385 386 Parameters 387 ---------- 388 todo_uuid : str, optional 389 A valid to-do uuid. 390 391 Returns 392 ------- 393 list of dict 394 Checklist items. 395 """ 396 database = pop_database(kwargs) 397 return database.get_checklist_items(todo_uuid=todo_uuid)
Read checklist items of to-dos into dicts.
Note: checklists are contained in the return value of
things.todos(todo_uuid)
and things.tasks(todo_uuid)
.
Parameters
- todo_uuid (str, optional): A valid to-do uuid.
Returns
- list of dict: Checklist items.
405def search(query: str, **kwargs) -> List[Dict]: 406 """ 407 Search tasks in the database. 408 409 Currently any part of a title and note of a to-do, project, 410 heading, or area is matched. 411 412 See the `search_query` parameter of `things.api.tasks` for details. 413 414 Examples 415 -------- 416 >>> things.search('Today%yellow') 417 [{'uuid': '6Hf2qWBjWhq7B1xszwdo34', 418 'type': 'to-do', 419 'title': 'Upcoming To-Do in Today (yellow)', 420 'status': 'incomplete', 421 'notes': '', 422 ... 423 """ 424 return tasks(search_query=query, **kwargs)
Search tasks in the database.
Currently any part of a title and note of a to-do, project, heading, or area is matched.
See the search_query
parameter of tasks
for details.
Examples
>>> things.search('Today%yellow')
[{'uuid': '6Hf2qWBjWhq7B1xszwdo34',
'type': 'to-do',
'title': 'Upcoming To-Do in Today (yellow)',
'status': 'incomplete',
'notes': '',
...
427def get(uuid, default=None, **kwargs): 428 """ 429 Find an object by uuid. If not found, return `default`. 430 431 Currently supports tasks, projects, headings, areas, and tags. 432 """ 433 try: 434 return tasks(uuid=uuid, **kwargs) 435 except ValueError: 436 pass 437 438 try: 439 return areas(uuid=uuid, **kwargs) 440 except ValueError: 441 pass 442 443 for tag in tags(**kwargs): 444 if tag["uuid"] == uuid: 445 return tag 446 447 return default
Find an object by uuid. If not found, return default
.
Currently supports tasks, projects, headings, areas, and tags.
453def todos(uuid=None, **kwargs): 454 """ 455 Read to-dos into dicts. 456 457 See `things.api.tasks` for details on the optional parameters. 458 """ 459 return tasks(uuid=uuid, type="to-do", **kwargs)
Read to-dos into dicts.
See tasks
for details on the optional parameters.
462def projects(uuid=None, **kwargs): 463 """ 464 Read projects into dicts. 465 466 See `things.api.tasks` for details on the optional parameters. 467 """ 468 return tasks(uuid=uuid, type="project", **kwargs)
Read projects into dicts.
See tasks
for details on the optional parameters.
474def inbox(**kwargs): 475 """ 476 Read Inbox into dicts. 477 478 See `things.api.tasks` for details on the optional parameters. 479 """ 480 return tasks(start="Inbox", **kwargs)
Read Inbox into dicts.
See tasks
for details on the optional parameters.
483def today(**kwargs): 484 """ 485 Read Today's tasks into dicts. 486 487 Note: The Things database reflects the state of the Things app when 488 it was last opened. For the Today tasks that means the database 489 might not be up to date anymore if you didn't open the app recently. 490 To get around this limitation, we here make a prediction of what 491 tasks would show up in Today if you were to open the app right now. 492 This prediction does not include repeating tasks at this time. 493 494 See `things.api.tasks` for details on the optional parameters. 495 """ 496 database = pop_database(kwargs) 497 kwargs["database"] = database 498 499 regular_today_tasks = tasks( 500 start_date=True, 501 start="Anytime", 502 index="todayIndex", 503 **kwargs, 504 ) 505 506 # Predictions of new to-dos indicated by a yellow dot in the app. 507 508 unconfirmed_scheduled_tasks = tasks( 509 start_date="past", 510 start="Someday", 511 index="todayIndex", 512 **kwargs, 513 ) 514 515 unconfirmed_overdue_tasks = tasks( 516 start_date=False, 517 deadline="past", 518 deadline_suppressed=False, 519 **kwargs, 520 ) 521 522 result = [ 523 *regular_today_tasks, 524 *unconfirmed_scheduled_tasks, 525 *unconfirmed_overdue_tasks, 526 ] 527 result.sort(key=lambda task: (task["today_index"], task["start_date"])) 528 529 return result
Read Today's tasks into dicts.
Note: The Things database reflects the state of the Things app when it was last opened. For the Today tasks that means the database might not be up to date anymore if you didn't open the app recently. To get around this limitation, we here make a prediction of what tasks would show up in Today if you were to open the app right now. This prediction does not include repeating tasks at this time.
See tasks
for details on the optional parameters.
532def upcoming(**kwargs): 533 """ 534 Read Upcoming tasks into dicts. 535 536 Note: unscheduled tasks with a deadline are not included here. 537 See the `things.api.deadline` function instead. 538 539 For details on parameters, see `things.api.tasks`. 540 """ 541 return tasks(start_date="future", start="Someday", **kwargs)
Read Upcoming tasks into dicts.
Note: unscheduled tasks with a deadline are not included here.
See the things.api.deadline
function instead.
For details on parameters, see tasks
.
544def anytime(**kwargs): 545 """ 546 Read Anytime tasks into dicts. 547 548 See `things.api.tasks` for details on the optional parameters. 549 """ 550 return tasks(start="Anytime", **kwargs)
Read Anytime tasks into dicts.
See tasks
for details on the optional parameters.
553def someday(**kwargs): 554 """ 555 Read Someday tasks into dicts. 556 557 See `things.api.tasks` for details on the optional parameters. 558 """ 559 return tasks(start_date=False, start="Someday", **kwargs)
Read Someday tasks into dicts.
See tasks
for details on the optional parameters.
562def logbook(**kwargs): 563 """ 564 Read Logbook tasks into dicts. 565 566 See `things.api.tasks` for details on the optional parameters. 567 """ 568 result = [*canceled(**kwargs), *completed(**kwargs)] 569 result.sort(key=lambda task: task["stop_date"], reverse=True) 570 return result
Read Logbook tasks into dicts.
See tasks
for details on the optional parameters.
573def trash(**kwargs): 574 """ 575 Read Trash tasks into dicts. 576 577 See `things.api.tasks` for details on the optional parameters. 578 """ 579 return tasks( 580 trashed=True, 581 context_trashed=kwargs.pop("context_trashed", None), 582 status=kwargs.pop("status", None), 583 **kwargs, 584 )
Read Trash tasks into dicts.
See tasks
for details on the optional parameters.
590def canceled(**kwargs): 591 """ 592 Read canceled tasks into dicts. 593 594 See `things.api.tasks` for details on the optional parameters. 595 """ 596 return tasks(status="canceled", **kwargs)
Read canceled tasks into dicts.
See tasks
for details on the optional parameters.
599def completed(**kwargs): 600 """ 601 Read completed tasks into dicts. 602 603 See `things.api.tasks` for details on the optional parameters. 604 605 Examples 606 -------- 607 >>> things.completed(count_only=True) 608 10 609 >>> things.completed(type='project', count_only=True) 610 0 611 >>> things.completed(type='to-do', last='1w') 612 [] 613 """ 614 return tasks(status="completed", **kwargs)
Read completed tasks into dicts.
See tasks
for details on the optional parameters.
Examples
>>> things.completed(count_only=True)
10
>>> things.completed(type='project', count_only=True)
0
>>> things.completed(type='to-do', last='1w')
[]
617def deadlines(**kwargs): 618 """ 619 Read tasks with deadlines into dicts. 620 621 See `things.api.tasks` for details on the optional parameters. 622 """ 623 result = tasks(deadline=True, **kwargs) 624 result.sort(key=lambda task: task["deadline"]) 625 return result
Read tasks with deadlines into dicts.
See tasks
for details on the optional parameters.
628def last(offset, **kwargs): 629 """ 630 Read tasks created within last X days, weeks, or years into dicts. 631 632 Per default, only incomplete tasks are included, but do see 633 `things.api.tasks` for details on the optional parameters. 634 635 Parameters 636 ---------- 637 offset : str 638 A valid date offset such as '3d', '5w', or '1y'. 639 For details, see the `last` parameter of `things.api.tasks`. 640 641 Returns 642 ------- 643 list of dict 644 Tasks within offset ordered by creation date. Newest first. 645 646 Examples 647 -------- 648 >>> things.last('3d') 649 [] 650 >>> things.last('1w', status='completed') 651 [] 652 >>> things.last('1y', tag='Important', status='completed', count_only=True) 653 0 654 655 """ 656 if offset is None: 657 raise ValueError(f"Invalid offset type: {offset!r}") 658 659 result = tasks(last=offset, **kwargs) 660 if result: 661 result.sort(key=lambda task: task["created"], reverse=True) 662 663 return result
Read tasks created within last X days, weeks, or years into dicts.
Per default, only incomplete tasks are included, but do see
tasks
for details on the optional parameters.
Parameters
- offset (str):
A valid date offset such as '3d', '5w', or '1y'.
For details, see the
last
parameter oftasks
.
Returns
- list of dict: Tasks within offset ordered by creation date. Newest first.
Examples
>>> things.last('3d')
[]
>>> things.last('1w', status='completed')
[]
>>> things.last('1y', tag='Important', status='completed', count_only=True)
0
669def token(**kwargs) -> Union[str, None]: 670 """ 671 Read the Things URL scheme authentication token. 672 673 You can make good use of this token to modify existing Things data 674 using the Things URL Scheme. For details, see 675 [here](https://culturedcode.com/things/help/url-scheme/). 676 """ 677 database = pop_database(kwargs) 678 return database.get_url_scheme_auth_token()
Read the Things URL scheme authentication token.
You can make good use of this token to modify existing Things data using the Things URL Scheme. For details, see here.
681def link(uuid): 682 """Return a things:///show?id=uuid link.""" 683 return f"things:///show?id={uuid}"
Return a things:///show?id=uuid link.
686def show(uuid): # noqa 687 """ 688 Show a certain uuid in the Things app. 689 690 Parameters 691 ---------- 692 uuid : str 693 A valid uuid of any Things object. 694 695 Examples 696 -------- 697 >>> tag = things.tags('Home') 698 >>> things.show(tag['uuid']) # doctest: +SKIP 699 """ 700 uri = link(uuid) 701 os.system(f"open {quote(uri)}")
Show a certain uuid in the Things app.
Parameters
- uuid (str): A valid uuid of any Things object.
Examples
>>> tag = things.tags('Home')
>>> things.show(tag['uuid']) # doctest: +SKIP
707def pop_database(kwargs): 708 """Instantiate non-default database from `kwargs` if provided.""" 709 filepath = kwargs.pop("filepath", None) 710 database = kwargs.pop("database", None) 711 print_sql = kwargs.pop("print_sql", False) 712 713 if not database: 714 database = Database(filepath=filepath, print_sql=print_sql) 715 return database
Instantiate non-default database from kwargs
if provided.