doingapp 0.5.1__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doingapp
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: A fast CLI task tracker — track what you're doing from the terminal.
5
5
  Author-email: Parker Grimes <parker@altellus.com>
6
6
  License-Expression: MIT
@@ -28,9 +28,9 @@ def _client() -> httpx.Client:
28
28
  return httpx.Client(base_url=BASE_URL, timeout=10.0, headers=headers)
29
29
 
30
30
 
31
- def register(email: str, password: str) -> dict:
31
+ def register(token: str, password: str) -> dict:
32
32
  with httpx.Client(base_url=BASE_URL, timeout=10.0) as c:
33
- r = c.post("/auth/register", json={"email": email, "password": password})
33
+ r = c.post("/auth/register", json={"token": token, "password": password})
34
34
  r.raise_for_status()
35
35
  return r.json()
36
36
 
@@ -134,6 +134,29 @@ def delete_task(task_id: int) -> None:
134
134
  r.raise_for_status()
135
135
 
136
136
 
137
+ def get_stats(context_id: int | None = None) -> dict:
138
+ params: dict = {"tz": _local_tz()}
139
+ if context_id is not None:
140
+ params["context_id"] = context_id
141
+ with _client() as c:
142
+ r = c.get("/stats", params=params)
143
+ r.raise_for_status()
144
+ return r.json()
145
+
146
+
147
+ def get_done_history(limit: int = 7, before: str | None = None,
148
+ context_id: int | None = None) -> dict:
149
+ params: dict = {"tz": _local_tz(), "limit": limit}
150
+ if before is not None:
151
+ params["before"] = before
152
+ if context_id is not None:
153
+ params["context_id"] = context_id
154
+ with _client() as c:
155
+ r = c.get("/done", params=params)
156
+ r.raise_for_status()
157
+ return r.json()
158
+
159
+
137
160
  def get_today(context_id: int | None = None) -> dict:
138
161
  params: dict = {"tz": _local_tz()}
139
162
  if context_id is not None:
@@ -161,6 +184,38 @@ def create_context(name: str) -> dict:
161
184
  return r.json()
162
185
 
163
186
 
187
+ def get_context(context_id: int) -> dict:
188
+ with _client() as c:
189
+ r = c.get(f"/contexts/{context_id}")
190
+ r.raise_for_status()
191
+ return r.json()
192
+
193
+
194
+ def update_context(context_id: int, name: str, icon: str | None = None) -> dict:
195
+ payload: dict = {"name": name}
196
+ if icon is not None:
197
+ payload["icon"] = icon
198
+ with _client() as c:
199
+ r = c.put(f"/contexts/{context_id}", json=payload)
200
+ r.raise_for_status()
201
+ return r.json()
202
+
203
+
204
+ def snooze_context(context_id: int) -> dict:
205
+ params: dict = {"tz": _local_tz()}
206
+ with _client() as c:
207
+ r = c.post(f"/contexts/{context_id}/snooze", params=params)
208
+ r.raise_for_status()
209
+ return r.json()
210
+
211
+
212
+ def unsnooze_context(context_id: int) -> dict:
213
+ with _client() as c:
214
+ r = c.post(f"/contexts/{context_id}/unsnooze")
215
+ r.raise_for_status()
216
+ return r.json()
217
+
218
+
164
219
  def delete_context(context_id: int) -> None:
165
220
  with _client() as c:
166
221
  r = c.delete(f"/contexts/{context_id}")
@@ -153,9 +153,10 @@ def login():
153
153
 
154
154
 
155
155
  @app.command()
156
- def register():
157
- """Create a new Doing account."""
158
- email = typer.prompt("Email")
156
+ def register(
157
+ token: str = typer.Argument(..., help="Invite token from your invitation link"),
158
+ ):
159
+ """Create a new Doing account using an invite token."""
159
160
  password = typer.prompt("Password", hide_input=True)
160
161
  password_confirm = typer.prompt("Confirm password", hide_input=True)
161
162
  if password != password_confirm:
@@ -164,13 +165,13 @@ def register():
164
165
  console.print("[bold red]Passwords do not match.[/bold red]")
165
166
  raise typer.Exit(code=1)
166
167
  try:
167
- data = api.register(email, password)
168
+ data = api.register(token, password)
168
169
  except Exception as exc:
169
170
  _handle_api_error(exc)
170
171
  save_token(data["access_token"])
171
172
  if _json_mode:
172
- _output_json({"ok": True, "email": email})
173
- console.print(f"[bold green]Account created![/bold green] Logged in as [bold]{email}[/bold]")
173
+ _output_json({"ok": True})
174
+ console.print("[bold green]Account created![/bold green] You're now logged in.")
174
175
 
175
176
 
176
177
  @app.command()
@@ -293,16 +294,28 @@ def log(
293
294
 
294
295
  @app.command()
295
296
  def done(
296
- task_id: int = typer.Argument(..., help="ID of the task to mark done"),
297
+ task_ids: list[int] = typer.Argument(..., help="ID(s) of the task(s) to mark done"),
297
298
  ):
298
- """Mark a task as done."""
299
- try:
300
- task = api.mark_done(task_id)
301
- except Exception as exc:
302
- _handle_api_error(exc)
299
+ """Mark one or more tasks as done."""
300
+ results = []
301
+ errors = []
302
+ for task_id in task_ids:
303
+ try:
304
+ task = api.mark_done(task_id)
305
+ except Exception as exc:
306
+ if _json_mode and len(task_ids) == 1:
307
+ _handle_api_error(exc)
308
+ if not _json_mode:
309
+ console.print(f"[red]Failed to complete task #{task_id}[/red]")
310
+ errors.append(task_id)
311
+ continue
312
+ results.append(task)
313
+ if not _json_mode:
314
+ console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]")
303
315
  if _json_mode:
304
- _output_json(task)
305
- console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]")
316
+ _output_json(results[0] if len(task_ids) == 1 and results else results)
317
+ if errors:
318
+ raise typer.Exit(code=1)
306
319
 
307
320
 
308
321
  @app.command()
@@ -362,7 +375,6 @@ def edit(
362
375
  if title is None and notes is None and context is None and not no_context:
363
376
  _validation_error("No changes specified. Use --title, --notes, --context, or --no-context.")
364
377
 
365
- # BUG-8: Validate title if provided
366
378
  if title is not None:
367
379
  title = _validate_title(title)
368
380
 
@@ -492,6 +504,64 @@ def context_add(
492
504
  console.print(f"[green bold]Created context:[/green bold] {ctx['name']} [dim](#{ctx['id']})[/dim]")
493
505
 
494
506
 
507
+ @context_app.command("edit")
508
+ def context_edit(
509
+ context_id: int = typer.Argument(..., help="Context ID to edit"),
510
+ name: str = typer.Option(None, "--name", "-n", help="New name"),
511
+ icon: str = typer.Option(None, "--icon", "-i", help="New icon (emoji)"),
512
+ ):
513
+ """Rename a context or change its icon."""
514
+ if name is None and icon is None:
515
+ _validation_error("No changes specified. Use --name or --icon.")
516
+
517
+ if name is not None:
518
+ name = _validate_name(name)
519
+
520
+ # Need current name if only changing icon (API requires name)
521
+ if name is None:
522
+ try:
523
+ current = api.get_context(context_id)
524
+ except Exception as exc:
525
+ _handle_api_error(exc)
526
+ name = current["name"]
527
+
528
+ try:
529
+ ctx = api.update_context(context_id, name=name, icon=icon)
530
+ except Exception as exc:
531
+ _handle_api_error(exc)
532
+ if _json_mode:
533
+ _output_json(ctx)
534
+ console.print(f"[green bold]Updated context:[/green bold] {ctx['name']} [dim](#{ctx['id']})[/dim]")
535
+
536
+
537
+ @context_app.command("snooze")
538
+ def context_snooze(
539
+ context_id: int = typer.Argument(..., help="Context ID to snooze"),
540
+ ):
541
+ """Snooze a context until end of day (hides its tasks)."""
542
+ try:
543
+ ctx = api.snooze_context(context_id)
544
+ except Exception as exc:
545
+ _handle_api_error(exc)
546
+ if _json_mode:
547
+ _output_json(ctx)
548
+ console.print(f"[yellow]😴 Snoozed:[/yellow] {ctx['name']} until end of day")
549
+
550
+
551
+ @context_app.command("unsnooze")
552
+ def context_unsnooze(
553
+ context_id: int = typer.Argument(..., help="Context ID to unsnooze"),
554
+ ):
555
+ """Unsnooze a context (show its tasks again)."""
556
+ try:
557
+ ctx = api.unsnooze_context(context_id)
558
+ except Exception as exc:
559
+ _handle_api_error(exc)
560
+ if _json_mode:
561
+ _output_json(ctx)
562
+ console.print(f"[green bold]Unsnoozed:[/green bold] {ctx['name']}")
563
+
564
+
495
565
  @context_app.command("rm")
496
566
  def context_rm(
497
567
  context_id: int = typer.Argument(..., help="Context ID to delete"),
@@ -506,5 +576,30 @@ def context_rm(
506
576
  console.print(f"[dim]Deleted context #{context_id}.[/dim]")
507
577
 
508
578
 
579
+ @app.command()
580
+ def stats(
581
+ context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
582
+ ):
583
+ """Show how many tasks you've completed today and this week."""
584
+ try:
585
+ data = api.get_stats(context_id=context)
586
+ except Exception as exc:
587
+ _handle_api_error(exc)
588
+
589
+ if _json_mode:
590
+ _output_json(data)
591
+
592
+ today_count = data["today"]
593
+ week_count = data["this_week"]
594
+ total_count = data["total"]
595
+
596
+ console.print(f"\n [bold]Today:[/bold] {today_count} task{'s' if today_count != 1 else ''} done")
597
+ console.print(f" [bold]This week:[/bold] {week_count} task{'s' if week_count != 1 else ''} done")
598
+
599
+ if total_count > week_count:
600
+ console.print(f" [dim]All time: {total_count} total[/dim]")
601
+ console.print()
602
+
603
+
509
604
  if __name__ == "__main__":
510
605
  app()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doingapp
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: A fast CLI task tracker — track what you're doing from the terminal.
5
5
  Author-email: Parker Grimes <parker@altellus.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "doingapp"
7
- version = "0.5.1"
7
+ version = "0.6.0"
8
8
  description = "A fast CLI task tracker — track what you're doing from the terminal."
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes