doingapp 0.4.0__tar.gz → 0.5.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.4.0
3
+ Version: 0.5.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
@@ -42,6 +42,13 @@ def login(email: str, password: str) -> dict:
42
42
  return r.json()
43
43
 
44
44
 
45
+ def get_me() -> dict:
46
+ with _client() as c:
47
+ r = c.get("/auth/me")
48
+ r.raise_for_status()
49
+ return r.json()
50
+
51
+
45
52
  def create_task(title: str, notes: str | None = None, context_id: int | None = None) -> dict:
46
53
  payload: dict = {"title": title}
47
54
  if notes is not None:
@@ -54,6 +61,13 @@ def create_task(title: str, notes: str | None = None, context_id: int | None = N
54
61
  return r.json()
55
62
 
56
63
 
64
+ def get_task(task_id: int) -> dict:
65
+ with _client() as c:
66
+ r = c.get(f"/tasks/{task_id}")
67
+ r.raise_for_status()
68
+ return r.json()
69
+
70
+
57
71
  def list_tasks(status: str | None = None, date: str | None = None,
58
72
  context_id: int | None = None) -> list[dict]:
59
73
  params: dict = {"tz": _local_tz()}
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import sys
2
3
 
3
4
  import httpx
4
5
  import typer
@@ -19,15 +20,41 @@ console = Console()
19
20
  # Commands that don't require authentication
20
21
  AUTH_COMMANDS = {"login", "register", "logout", "apikey"}
21
22
 
23
+ # Global JSON mode flag, set by --json on any command
24
+ _json_mode = False
25
+
26
+
27
+ def _output_json(data) -> None:
28
+ """Write raw JSON to stdout (no Rich formatting) and exit."""
29
+ sys.stdout.write(json.dumps(data) + "\n")
30
+ raise typer.Exit()
31
+
22
32
 
23
33
  def _require_auth():
24
34
  """Exit with a message if the user is not logged in."""
25
35
  if load_token() is None:
36
+ if _json_mode:
37
+ _output_json({"error": True, "detail": "Not logged in"})
26
38
  console.print("[bold red]Not logged in.[/bold red] Run: [bold]doing login[/bold]")
27
39
  raise typer.Exit(code=1)
28
40
 
29
41
 
30
42
  def _handle_api_error(exc: Exception):
43
+ if _json_mode:
44
+ error: dict = {"error": True}
45
+ if isinstance(exc, httpx.HTTPStatusError):
46
+ error["status"] = exc.response.status_code
47
+ try:
48
+ error["detail"] = exc.response.json().get("detail", exc.response.text)
49
+ except Exception:
50
+ error["detail"] = exc.response.text
51
+ elif isinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
52
+ error["detail"] = f"Could not connect to {api.BASE_URL}"
53
+ else:
54
+ error["detail"] = str(exc)
55
+ sys.stdout.write(json.dumps(error) + "\n")
56
+ raise typer.Exit(code=1)
57
+
31
58
  if isinstance(exc, httpx.HTTPStatusError):
32
59
  status = exc.response.status_code
33
60
  if status == 401:
@@ -72,11 +99,16 @@ def _print_done_task(task: dict) -> None:
72
99
 
73
100
 
74
101
  @app.callback()
75
- def main(ctx: typer.Context):
102
+ def main(
103
+ ctx: typer.Context,
104
+ json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON (for scripting and AI agents)"),
105
+ ):
76
106
  """Doing - track what you're working on."""
107
+ global _json_mode
108
+ _json_mode = json_output
77
109
  if ctx.invoked_subcommand is None:
78
110
  _require_auth()
79
- ls(context=None, json_output=False)
111
+ ls()
80
112
  elif ctx.invoked_subcommand not in AUTH_COMMANDS:
81
113
  _require_auth()
82
114
 
@@ -91,6 +123,8 @@ def login():
91
123
  except Exception as exc:
92
124
  _handle_api_error(exc)
93
125
  save_token(data["access_token"])
126
+ if _json_mode:
127
+ _output_json({"ok": True, "email": email})
94
128
  console.print(f"[bold green]Logged in[/bold green] as [bold]{email}[/bold]")
95
129
 
96
130
 
@@ -101,6 +135,8 @@ def register():
101
135
  password = typer.prompt("Password", hide_input=True)
102
136
  password_confirm = typer.prompt("Confirm password", hide_input=True)
103
137
  if password != password_confirm:
138
+ if _json_mode:
139
+ _output_json({"error": True, "detail": "Passwords do not match"})
104
140
  console.print("[bold red]Passwords do not match.[/bold red]")
105
141
  raise typer.Exit(code=1)
106
142
  try:
@@ -108,6 +144,8 @@ def register():
108
144
  except Exception as exc:
109
145
  _handle_api_error(exc)
110
146
  save_token(data["access_token"])
147
+ if _json_mode:
148
+ _output_json({"ok": True, "email": email})
111
149
  console.print(f"[bold green]Account created![/bold green] Logged in as [bold]{email}[/bold]")
112
150
 
113
151
 
@@ -115,6 +153,8 @@ def register():
115
153
  def logout():
116
154
  """Log out and clear stored credentials."""
117
155
  clear_token()
156
+ if _json_mode:
157
+ _output_json({"ok": True})
118
158
  console.print("[dim]Logged out.[/dim]")
119
159
 
120
160
 
@@ -126,12 +166,30 @@ def apikey(
126
166
  if key is None:
127
167
  key = typer.prompt("API key", hide_input=True)
128
168
  if not key.startswith("doing_"):
169
+ if _json_mode:
170
+ _output_json({"error": True, "detail": "Invalid API key. Keys start with doing_"})
129
171
  console.print("[bold red]Invalid API key.[/bold red] Keys start with [bold]doing_[/bold]")
130
172
  raise typer.Exit(code=1)
131
173
  save_token(key)
174
+ if _json_mode:
175
+ _output_json({"ok": True})
132
176
  console.print("[bold green]API key saved.[/bold green] You're all set.")
133
177
 
134
178
 
179
+ @app.command()
180
+ def me():
181
+ """Show current user info (useful for verifying auth)."""
182
+ try:
183
+ user = api.get_me()
184
+ except Exception as exc:
185
+ _handle_api_error(exc)
186
+ if _json_mode:
187
+ _output_json(user)
188
+ console.print(f" [bold]{user['email']}[/bold] [dim](#{user['id']})[/dim]")
189
+ if user.get("is_admin"):
190
+ console.print(" [yellow]Admin[/yellow]")
191
+
192
+
135
193
  @app.command()
136
194
  def add(
137
195
  title: str = typer.Argument(..., help="What are you working on?"),
@@ -143,22 +201,42 @@ def add(
143
201
  task = api.create_task(title, notes, context)
144
202
  except Exception as exc:
145
203
  _handle_api_error(exc)
204
+ if _json_mode:
205
+ _output_json(task)
146
206
  console.print(f"[green bold]●[/green bold] Added: [bold]{task['title']}[/bold] [dim](#{task['id']})[/dim]")
147
207
 
148
208
 
209
+ @app.command()
210
+ def get(
211
+ task_id: int = typer.Argument(..., help="ID of the task to fetch"),
212
+ ):
213
+ """Get a single task by ID."""
214
+ try:
215
+ task = api.get_task(task_id)
216
+ except Exception as exc:
217
+ _handle_api_error(exc)
218
+ if _json_mode:
219
+ _output_json(task)
220
+ status_icon = {"active": "[green bold]●[/green bold]", "paused": "[yellow]⏸[/yellow]", "done": "[green]✓[/green]"}
221
+ icon = status_icon.get(task["status"], "?")
222
+ ctx = f" [dim][{task['context_name']}][/dim]" if task.get("context_name") else ""
223
+ console.print(f" {icon} [dim]#{task['id']}[/dim] {task['title']}{ctx}")
224
+ if task.get("notes"):
225
+ console.print(f" [dim]{task['notes']}[/dim]")
226
+ console.print(f" [dim]Status: {task['status']} Created: {task['created_at']}[/dim]")
227
+
228
+
149
229
  @app.command()
150
230
  def ls(
151
231
  context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
152
- json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
153
232
  ):
154
233
  """Show what's on your plate right now."""
155
234
  try:
156
235
  tasks = api.list_tasks(status="active", context_id=context)
157
236
  except Exception as exc:
158
237
  _handle_api_error(exc)
159
- if json_output:
160
- console.print_json(json.dumps(tasks))
161
- raise typer.Exit()
238
+ if _json_mode:
239
+ _output_json(tasks)
162
240
  if not tasks:
163
241
  console.print("Nothing on your plate. Add something: [bold]doing add 'task name'[/bold]")
164
242
  raise typer.Exit()
@@ -170,7 +248,6 @@ def ls(
170
248
  def log(
171
249
  date: str = typer.Option(None, "--date", "-d", help="Date (YYYY-MM-DD), defaults to today"),
172
250
  context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
173
- json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
174
251
  ):
175
252
  """List tasks marked done today (or on a given date)."""
176
253
  from datetime import date as date_cls
@@ -179,9 +256,8 @@ def log(
179
256
  tasks = api.list_tasks(status="done", date=query_date, context_id=context)
180
257
  except Exception as exc:
181
258
  _handle_api_error(exc)
182
- if json_output:
183
- console.print_json(json.dumps(tasks))
184
- raise typer.Exit()
259
+ if _json_mode:
260
+ _output_json(tasks)
185
261
  if not tasks:
186
262
  label = query_date if date else "today"
187
263
  console.print(f"Nothing marked done {label}.")
@@ -199,6 +275,8 @@ def done(
199
275
  task = api.mark_done(task_id)
200
276
  except Exception as exc:
201
277
  _handle_api_error(exc)
278
+ if _json_mode:
279
+ _output_json(task)
202
280
  console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]")
203
281
 
204
282
 
@@ -211,6 +289,8 @@ def reopen(
211
289
  task = api.reopen_task(task_id)
212
290
  except Exception as exc:
213
291
  _handle_api_error(exc)
292
+ if _json_mode:
293
+ _output_json(task)
214
294
  console.print(f"[green bold]●[/green bold] Reopened: [bold]{task['title']}[/bold]")
215
295
 
216
296
 
@@ -223,6 +303,8 @@ def pause(
223
303
  task = api.pause_task(task_id)
224
304
  except Exception as exc:
225
305
  _handle_api_error(exc)
306
+ if _json_mode:
307
+ _output_json(task)
226
308
  console.print(f"[yellow]⏸[/yellow] Paused: [bold]{task['title']}[/bold]")
227
309
 
228
310
 
@@ -235,6 +317,8 @@ def resume(
235
317
  task = api.resume_task(task_id)
236
318
  except Exception as exc:
237
319
  _handle_api_error(exc)
320
+ if _json_mode:
321
+ _output_json(task)
238
322
  console.print(f"[green bold]●[/green bold] Resumed: [bold]{task['title']}[/bold]")
239
323
 
240
324
 
@@ -252,13 +336,14 @@ def edit(
252
336
  context_id=context, clear_context=no_context)
253
337
  except Exception as exc:
254
338
  _handle_api_error(exc)
339
+ if _json_mode:
340
+ _output_json(task)
255
341
  console.print(f"[green bold]Updated:[/green bold] {task['title']} [dim](#{task['id']})[/dim]")
256
342
 
257
343
 
258
344
  @app.command()
259
345
  def today(
260
346
  context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
261
- json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
262
347
  ):
263
348
  """Show everything from today, grouped by status."""
264
349
  try:
@@ -266,9 +351,8 @@ def today(
266
351
  except Exception as exc:
267
352
  _handle_api_error(exc)
268
353
 
269
- if json_output:
270
- console.print_json(json.dumps(data))
271
- raise typer.Exit()
354
+ if _json_mode:
355
+ _output_json(data)
272
356
 
273
357
  active = data.get("active", [])
274
358
  paused = data.get("paused", [])
@@ -305,6 +389,8 @@ def rm(
305
389
  api.delete_task(task_id)
306
390
  except Exception as exc:
307
391
  _handle_api_error(exc)
392
+ if _json_mode:
393
+ _output_json({"deleted": True, "id": task_id})
308
394
  console.print(f"[dim]Deleted task #{task_id}.[/dim]")
309
395
 
310
396
 
@@ -317,16 +403,25 @@ def clear():
317
403
  _handle_api_error(exc)
318
404
 
319
405
  if not tasks:
406
+ if _json_mode:
407
+ _output_json([])
320
408
  console.print("Nothing to clear.")
321
409
  raise typer.Exit()
322
410
 
411
+ results = []
323
412
  for t in tasks:
324
413
  try:
325
- api.mark_done(t["id"])
414
+ result = api.mark_done(t["id"])
415
+ results.append(result)
326
416
  except Exception:
327
- console.print(f"[red]Failed to complete task #{t['id']}[/red]")
417
+ if not _json_mode:
418
+ console.print(f"[red]Failed to complete task #{t['id']}[/red]")
328
419
  continue
329
- console.print(f"[green]✓[/green] Done: [bold]{t['title']}[/bold]")
420
+ if not _json_mode:
421
+ console.print(f"[green]✓[/green] Done: [bold]{t['title']}[/bold]")
422
+
423
+ if _json_mode:
424
+ _output_json(results)
330
425
 
331
426
  console.print(f"\n[dim]Cleared {len(tasks)} task(s).[/dim]")
332
427
 
@@ -338,6 +433,8 @@ def contexts():
338
433
  ctx_list = api.list_contexts()
339
434
  except Exception as exc:
340
435
  _handle_api_error(exc)
436
+ if _json_mode:
437
+ _output_json(ctx_list)
341
438
  if not ctx_list:
342
439
  console.print("No contexts. Create one: [bold]doing context add 'name'[/bold]")
343
440
  raise typer.Exit()
@@ -355,6 +452,8 @@ def context_add(
355
452
  ctx = api.create_context(name)
356
453
  except Exception as exc:
357
454
  _handle_api_error(exc)
455
+ if _json_mode:
456
+ _output_json(ctx)
358
457
  console.print(f"[green bold]Created context:[/green bold] {ctx['name']} [dim](#{ctx['id']})[/dim]")
359
458
 
360
459
 
@@ -368,6 +467,8 @@ def context_rm(
368
467
  api.delete_context(context_id)
369
468
  except Exception as exc:
370
469
  _handle_api_error(exc)
470
+ if _json_mode:
471
+ _output_json({"deleted": True, "id": context_id})
371
472
  console.print(f"[dim]Deleted context #{context_id}.[/dim]")
372
473
 
373
474
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doingapp
3
- Version: 0.4.0
3
+ Version: 0.5.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.4.0"
7
+ version = "0.5.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