doingapp 0.3.1__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.3.1
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,12 +61,22 @@ def create_task(title: str, notes: str | None = None, context_id: int | None = N
54
61
  return r.json()
55
62
 
56
63
 
57
- def list_tasks(status: str | None = None, date: str | None = None) -> list[dict]:
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
+
71
+ def list_tasks(status: str | None = None, date: str | None = None,
72
+ context_id: int | None = None) -> list[dict]:
58
73
  params: dict = {"tz": _local_tz()}
59
74
  if status is not None:
60
75
  params["status"] = status
61
76
  if date is not None:
62
77
  params["date"] = date
78
+ if context_id is not None:
79
+ params["context_id"] = context_id
63
80
  with _client() as c:
64
81
  r = c.get("/tasks", params=params)
65
82
  r.raise_for_status()
@@ -117,9 +134,12 @@ def delete_task(task_id: int) -> None:
117
134
  r.raise_for_status()
118
135
 
119
136
 
120
- def get_today() -> dict:
137
+ def get_today(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
121
141
  with _client() as c:
122
- r = c.get("/today", params={"tz": _local_tz()})
142
+ r = c.get("/today", params=params)
123
143
  r.raise_for_status()
124
144
  return r.json()
125
145
 
@@ -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(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,21 +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
- json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
231
+ context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
152
232
  ):
153
233
  """Show what's on your plate right now."""
154
234
  try:
155
- tasks = api.list_tasks(status="active")
235
+ tasks = api.list_tasks(status="active", context_id=context)
156
236
  except Exception as exc:
157
237
  _handle_api_error(exc)
158
- if json_output:
159
- console.print_json(json.dumps(tasks))
160
- raise typer.Exit()
238
+ if _json_mode:
239
+ _output_json(tasks)
161
240
  if not tasks:
162
241
  console.print("Nothing on your plate. Add something: [bold]doing add 'task name'[/bold]")
163
242
  raise typer.Exit()
@@ -168,18 +247,17 @@ def ls(
168
247
  @app.command()
169
248
  def log(
170
249
  date: str = typer.Option(None, "--date", "-d", help="Date (YYYY-MM-DD), defaults to today"),
171
- json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
250
+ context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
172
251
  ):
173
252
  """List tasks marked done today (or on a given date)."""
174
253
  from datetime import date as date_cls
175
254
  query_date = date if date else date_cls.today().isoformat()
176
255
  try:
177
- tasks = api.list_tasks(status="done", date=query_date)
256
+ tasks = api.list_tasks(status="done", date=query_date, context_id=context)
178
257
  except Exception as exc:
179
258
  _handle_api_error(exc)
180
- if json_output:
181
- console.print_json(json.dumps(tasks))
182
- raise typer.Exit()
259
+ if _json_mode:
260
+ _output_json(tasks)
183
261
  if not tasks:
184
262
  label = query_date if date else "today"
185
263
  console.print(f"Nothing marked done {label}.")
@@ -197,6 +275,8 @@ def done(
197
275
  task = api.mark_done(task_id)
198
276
  except Exception as exc:
199
277
  _handle_api_error(exc)
278
+ if _json_mode:
279
+ _output_json(task)
200
280
  console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]")
201
281
 
202
282
 
@@ -209,6 +289,8 @@ def reopen(
209
289
  task = api.reopen_task(task_id)
210
290
  except Exception as exc:
211
291
  _handle_api_error(exc)
292
+ if _json_mode:
293
+ _output_json(task)
212
294
  console.print(f"[green bold]●[/green bold] Reopened: [bold]{task['title']}[/bold]")
213
295
 
214
296
 
@@ -221,6 +303,8 @@ def pause(
221
303
  task = api.pause_task(task_id)
222
304
  except Exception as exc:
223
305
  _handle_api_error(exc)
306
+ if _json_mode:
307
+ _output_json(task)
224
308
  console.print(f"[yellow]⏸[/yellow] Paused: [bold]{task['title']}[/bold]")
225
309
 
226
310
 
@@ -233,6 +317,8 @@ def resume(
233
317
  task = api.resume_task(task_id)
234
318
  except Exception as exc:
235
319
  _handle_api_error(exc)
320
+ if _json_mode:
321
+ _output_json(task)
236
322
  console.print(f"[green bold]●[/green bold] Resumed: [bold]{task['title']}[/bold]")
237
323
 
238
324
 
@@ -250,22 +336,23 @@ def edit(
250
336
  context_id=context, clear_context=no_context)
251
337
  except Exception as exc:
252
338
  _handle_api_error(exc)
339
+ if _json_mode:
340
+ _output_json(task)
253
341
  console.print(f"[green bold]Updated:[/green bold] {task['title']} [dim](#{task['id']})[/dim]")
254
342
 
255
343
 
256
344
  @app.command()
257
345
  def today(
258
- json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
346
+ context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
259
347
  ):
260
348
  """Show everything from today, grouped by status."""
261
349
  try:
262
- data = api.get_today()
350
+ data = api.get_today(context_id=context)
263
351
  except Exception as exc:
264
352
  _handle_api_error(exc)
265
353
 
266
- if json_output:
267
- console.print_json(json.dumps(data))
268
- raise typer.Exit()
354
+ if _json_mode:
355
+ _output_json(data)
269
356
 
270
357
  active = data.get("active", [])
271
358
  paused = data.get("paused", [])
@@ -302,6 +389,8 @@ def rm(
302
389
  api.delete_task(task_id)
303
390
  except Exception as exc:
304
391
  _handle_api_error(exc)
392
+ if _json_mode:
393
+ _output_json({"deleted": True, "id": task_id})
305
394
  console.print(f"[dim]Deleted task #{task_id}.[/dim]")
306
395
 
307
396
 
@@ -314,16 +403,25 @@ def clear():
314
403
  _handle_api_error(exc)
315
404
 
316
405
  if not tasks:
406
+ if _json_mode:
407
+ _output_json([])
317
408
  console.print("Nothing to clear.")
318
409
  raise typer.Exit()
319
410
 
411
+ results = []
320
412
  for t in tasks:
321
413
  try:
322
- api.mark_done(t["id"])
414
+ result = api.mark_done(t["id"])
415
+ results.append(result)
323
416
  except Exception:
324
- 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]")
325
419
  continue
326
- 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)
327
425
 
328
426
  console.print(f"\n[dim]Cleared {len(tasks)} task(s).[/dim]")
329
427
 
@@ -335,6 +433,8 @@ def contexts():
335
433
  ctx_list = api.list_contexts()
336
434
  except Exception as exc:
337
435
  _handle_api_error(exc)
436
+ if _json_mode:
437
+ _output_json(ctx_list)
338
438
  if not ctx_list:
339
439
  console.print("No contexts. Create one: [bold]doing context add 'name'[/bold]")
340
440
  raise typer.Exit()
@@ -352,6 +452,8 @@ def context_add(
352
452
  ctx = api.create_context(name)
353
453
  except Exception as exc:
354
454
  _handle_api_error(exc)
455
+ if _json_mode:
456
+ _output_json(ctx)
355
457
  console.print(f"[green bold]Created context:[/green bold] {ctx['name']} [dim](#{ctx['id']})[/dim]")
356
458
 
357
459
 
@@ -365,6 +467,8 @@ def context_rm(
365
467
  api.delete_context(context_id)
366
468
  except Exception as exc:
367
469
  _handle_api_error(exc)
470
+ if _json_mode:
471
+ _output_json({"deleted": True, "id": context_id})
368
472
  console.print(f"[dim]Deleted context #{context_id}.[/dim]")
369
473
 
370
474
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doingapp
3
- Version: 0.3.1
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.3.1"
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