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.
- {doingapp-0.3.1 → doingapp-0.5.0}/PKG-INFO +1 -1
- {doingapp-0.3.1 → doingapp-0.5.0}/doing_cli/api.py +23 -3
- {doingapp-0.3.1 → doingapp-0.5.0}/doing_cli/main.py +124 -20
- {doingapp-0.3.1 → doingapp-0.5.0}/doingapp.egg-info/PKG-INFO +1 -1
- {doingapp-0.3.1 → doingapp-0.5.0}/pyproject.toml +1 -1
- {doingapp-0.3.1 → doingapp-0.5.0}/LICENSE +0 -0
- {doingapp-0.3.1 → doingapp-0.5.0}/README.md +0 -0
- {doingapp-0.3.1 → doingapp-0.5.0}/doing_cli/__init__.py +0 -0
- {doingapp-0.3.1 → doingapp-0.5.0}/doing_cli/auth.py +0 -0
- {doingapp-0.3.1 → doingapp-0.5.0}/doingapp.egg-info/SOURCES.txt +0 -0
- {doingapp-0.3.1 → doingapp-0.5.0}/doingapp.egg-info/dependency_links.txt +0 -0
- {doingapp-0.3.1 → doingapp-0.5.0}/doingapp.egg-info/entry_points.txt +0 -0
- {doingapp-0.3.1 → doingapp-0.5.0}/doingapp.egg-info/requires.txt +0 -0
- {doingapp-0.3.1 → doingapp-0.5.0}/doingapp.egg-info/top_level.txt +0 -0
- {doingapp-0.3.1 → 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,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
|
|
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=
|
|
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(
|
|
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,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
|
-
|
|
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
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
181
|
-
|
|
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
|
-
|
|
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
|
|
267
|
-
|
|
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
|
-
|
|
417
|
+
if not _json_mode:
|
|
418
|
+
console.print(f"[red]Failed to complete task #{t['id']}[/red]")
|
|
325
419
|
continue
|
|
326
|
-
|
|
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
|
|
|
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
|