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.
- {doingapp-0.5.1 → doingapp-0.6.0}/PKG-INFO +1 -1
- {doingapp-0.5.1 → doingapp-0.6.0}/doing_cli/api.py +57 -2
- {doingapp-0.5.1 → doingapp-0.6.0}/doing_cli/main.py +110 -15
- {doingapp-0.5.1 → doingapp-0.6.0}/doingapp.egg-info/PKG-INFO +1 -1
- {doingapp-0.5.1 → doingapp-0.6.0}/pyproject.toml +1 -1
- {doingapp-0.5.1 → doingapp-0.6.0}/LICENSE +0 -0
- {doingapp-0.5.1 → doingapp-0.6.0}/README.md +0 -0
- {doingapp-0.5.1 → doingapp-0.6.0}/doing_cli/__init__.py +0 -0
- {doingapp-0.5.1 → doingapp-0.6.0}/doing_cli/auth.py +0 -0
- {doingapp-0.5.1 → doingapp-0.6.0}/doingapp.egg-info/SOURCES.txt +0 -0
- {doingapp-0.5.1 → doingapp-0.6.0}/doingapp.egg-info/dependency_links.txt +0 -0
- {doingapp-0.5.1 → doingapp-0.6.0}/doingapp.egg-info/entry_points.txt +0 -0
- {doingapp-0.5.1 → doingapp-0.6.0}/doingapp.egg-info/requires.txt +0 -0
- {doingapp-0.5.1 → doingapp-0.6.0}/doingapp.egg-info/top_level.txt +0 -0
- {doingapp-0.5.1 → doingapp-0.6.0}/setup.cfg +0 -0
|
@@ -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(
|
|
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={"
|
|
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
|
-
"
|
|
158
|
-
|
|
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(
|
|
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
|
|
173
|
-
console.print(
|
|
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
|
-
|
|
297
|
+
task_ids: list[int] = typer.Argument(..., help="ID(s) of the task(s) to mark done"),
|
|
297
298
|
):
|
|
298
|
-
"""Mark
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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(
|
|
305
|
-
|
|
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()
|
|
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
|