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.
- {doingapp-0.4.0 → doingapp-0.5.0}/PKG-INFO +1 -1
- {doingapp-0.4.0 → doingapp-0.5.0}/doing_cli/api.py +14 -0
- {doingapp-0.4.0 → doingapp-0.5.0}/doing_cli/main.py +118 -17
- {doingapp-0.4.0 → doingapp-0.5.0}/doingapp.egg-info/PKG-INFO +1 -1
- {doingapp-0.4.0 → doingapp-0.5.0}/pyproject.toml +1 -1
- {doingapp-0.4.0 → doingapp-0.5.0}/LICENSE +0 -0
- {doingapp-0.4.0 → doingapp-0.5.0}/README.md +0 -0
- {doingapp-0.4.0 → doingapp-0.5.0}/doing_cli/__init__.py +0 -0
- {doingapp-0.4.0 → doingapp-0.5.0}/doing_cli/auth.py +0 -0
- {doingapp-0.4.0 → doingapp-0.5.0}/doingapp.egg-info/SOURCES.txt +0 -0
- {doingapp-0.4.0 → doingapp-0.5.0}/doingapp.egg-info/dependency_links.txt +0 -0
- {doingapp-0.4.0 → doingapp-0.5.0}/doingapp.egg-info/entry_points.txt +0 -0
- {doingapp-0.4.0 → doingapp-0.5.0}/doingapp.egg-info/requires.txt +0 -0
- {doingapp-0.4.0 → doingapp-0.5.0}/doingapp.egg-info/top_level.txt +0 -0
- {doingapp-0.4.0 → doingapp-0.5.0}/setup.cfg +0 -0
|
@@ -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(
|
|
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(
|
|
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
|
|
160
|
-
|
|
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
|
|
183
|
-
|
|
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
|
|
270
|
-
|
|
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
|
-
|
|
417
|
+
if not _json_mode:
|
|
418
|
+
console.print(f"[red]Failed to complete task #{t['id']}[/red]")
|
|
328
419
|
continue
|
|
329
|
-
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|