podojo-cli 0.4.4__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.
Files changed (35) hide show
  1. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/CHANGELOG.md +11 -0
  2. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/PKG-INFO +1 -1
  3. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/pyproject.toml +1 -1
  4. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/client.py +34 -0
  5. podojo_cli-0.5.0/src/podojo_cli/commands/interviews.py +57 -0
  6. podojo_cli-0.5.0/src/podojo_cli/commands/projects.py +47 -0
  7. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/transcripts.py +12 -1
  8. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/videos.py +14 -0
  9. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/main.py +2 -1
  10. podojo_cli-0.5.0/tests/test_interviews.py +86 -0
  11. podojo_cli-0.5.0/tests/test_projects.py +72 -0
  12. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/uv.lock +1 -1
  13. podojo_cli-0.4.4/src/podojo_cli/commands/projects.py +0 -27
  14. podojo_cli-0.4.4/tests/test_projects.py +0 -38
  15. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/.github/workflows/publish.yml +0 -0
  16. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/.gitignore +0 -0
  17. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/CLAUDE.md +0 -0
  18. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/README.md +0 -0
  19. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/__init__.py +0 -0
  20. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/__init__.py +0 -0
  21. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/auth.py +0 -0
  22. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/gdrive.py +0 -0
  23. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/showreel.py +0 -0
  24. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/usertests.py +0 -0
  25. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/config.py +0 -0
  26. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/gdrive/__init__.py +0 -0
  27. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/gdrive/list.py +0 -0
  28. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/gdrive/upload.py +0 -0
  29. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/video/__init__.py +0 -0
  30. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/video/showreel.py +0 -0
  31. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/tests/conftest.py +0 -0
  32. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/tests/test_auth.py +0 -0
  33. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/tests/test_gdrive.py +0 -0
  34. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/tests/test_showreel.py +0 -0
  35. {podojo_cli-0.4.4 → podojo_cli-0.5.0}/tests/test_usertests.py +0 -0
@@ -5,6 +5,17 @@ All notable changes to the Podojo CLI will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org).
7
7
 
8
+ ## [0.5.0] - 2026-05-01
9
+
10
+ ### Added
11
+ - `interviews upload` command uploads audio/video interview files to an existing project
12
+ - `projects create` command creates a new empty project (uploads now require a pre-existing project, no auto-creation)
13
+
14
+ ## [0.4.5] - 2026-04-25
15
+
16
+ ### Added
17
+ - Show interview date/time in `transcripts list` and `videos list` tables
18
+
8
19
  ## [0.4.4] - 2026-04-19
9
20
 
10
21
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: podojo-cli
3
- Version: 0.4.4
3
+ Version: 0.5.0
4
4
  Summary: CLI for the Podojo user research platform
5
5
  Project-URL: Homepage, https://github.com/podojo/cli-podojo
6
6
  Project-URL: Source, https://github.com/podojo/cli-podojo
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "podojo-cli"
3
- version = "0.4.4"
3
+ version = "0.5.0"
4
4
  description = "CLI for the Podojo user research platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,3 +1,6 @@
1
+ import urllib.parse
2
+ from pathlib import Path
3
+
1
4
  import httpx
2
5
 
3
6
  from .config import load_config
@@ -21,6 +24,37 @@ class PodojoClient:
21
24
  r.raise_for_status()
22
25
  return r.json()["projects"]
23
26
 
27
+ def create_project(self, name: str, brief: str = "") -> dict:
28
+ r = httpx.post(
29
+ f"{self.base_url}/projects",
30
+ json={"name": name, "brief": brief},
31
+ headers=self._headers(),
32
+ )
33
+ r.raise_for_status()
34
+ return r.json()
35
+
36
+ def upload_interview(
37
+ self,
38
+ project: str,
39
+ file_path: Path,
40
+ audio_only: bool = False,
41
+ batch_name: str | None = None,
42
+ ) -> dict:
43
+ encoded = urllib.parse.quote(project, safe="")
44
+ data = {"audio_only": str(audio_only).lower()}
45
+ if batch_name:
46
+ data["batch_name"] = batch_name
47
+ with file_path.open("rb") as f:
48
+ r = httpx.post(
49
+ f"{self.base_url}/projects/{encoded}/interviews",
50
+ files={"file": (file_path.name, f)},
51
+ data=data,
52
+ headers=self._headers(),
53
+ timeout=httpx.Timeout(None),
54
+ )
55
+ r.raise_for_status()
56
+ return r.json()
57
+
24
58
  def list_transcripts(self, project: str) -> dict:
25
59
  r = httpx.get(
26
60
  f"{self.base_url}/projects/{project}/transcripts",
@@ -0,0 +1,57 @@
1
+ from pathlib import Path
2
+
3
+ import httpx
4
+ import typer
5
+ from rich.console import Console
6
+
7
+ from ..client import PodojoClient
8
+
9
+ app = typer.Typer(help="Manage interviews")
10
+ console = Console()
11
+
12
+
13
+ @app.command("upload")
14
+ def upload_interview(
15
+ file: Path = typer.Argument(
16
+ ...,
17
+ exists=True,
18
+ dir_okay=False,
19
+ readable=True,
20
+ help="Audio/video file to upload",
21
+ ),
22
+ project: str = typer.Option(..., "--project", "-p", help="Existing project name"),
23
+ audio_only: bool = typer.Option(False, "--audio-only", help="Skip video processing"),
24
+ batch_name: str = typer.Option(
25
+ None, "--name", "-n", help="Override interview name (defaults to filename)"
26
+ ),
27
+ ):
28
+ """Upload an audio/video interview file to an existing project."""
29
+ client = PodojoClient()
30
+ size_mb = file.stat().st_size / (1024 * 1024)
31
+ with console.status(f"Uploading {file.name} ({size_mb:.1f} MB)..."):
32
+ try:
33
+ result = client.upload_interview(
34
+ project, file, audio_only=audio_only, batch_name=batch_name
35
+ )
36
+ except httpx.HTTPStatusError as e:
37
+ status = e.response.status_code
38
+ detail = ""
39
+ try:
40
+ detail = e.response.json().get("detail", "")
41
+ except Exception:
42
+ detail = e.response.text
43
+ if status == 404:
44
+ console.print(
45
+ f"[red]Project '{project}' not found.[/red] "
46
+ f"Create it first: [bold]podojo projects create \"{project}\"[/bold]"
47
+ )
48
+ elif status == 400:
49
+ console.print(f"[red]{detail}[/red]")
50
+ else:
51
+ console.print(f"[red]{status}: {detail}[/red]")
52
+ raise typer.Exit(code=1)
53
+
54
+ console.print(
55
+ f"Uploaded [bold]{file.name}[/bold] → {project} "
56
+ f"(batch_id={result['batch_id']})"
57
+ )
@@ -0,0 +1,47 @@
1
+ import httpx
2
+ import typer
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+
6
+ from ..client import PodojoClient
7
+
8
+ app = typer.Typer(help="Manage projects")
9
+ console = Console()
10
+
11
+
12
+ @app.command("list")
13
+ def list_projects(
14
+ skip: int = typer.Option(0, help="Number of projects to skip"),
15
+ limit: int = typer.Option(50, help="Max projects to return"),
16
+ ):
17
+ """List all projects."""
18
+ client = PodojoClient()
19
+ projects = client.list_projects(skip=skip, limit=limit)
20
+
21
+ table = Table(title="Projects")
22
+ table.add_column("Name")
23
+ table.add_column("Brief")
24
+
25
+ for p in projects:
26
+ table.add_row(p.get("name", ""), p.get("brief", ""))
27
+
28
+ console.print(table)
29
+
30
+
31
+ @app.command("create")
32
+ def create_project(
33
+ name: str = typer.Argument(help="Project name"),
34
+ brief: str = typer.Option("", "--brief", "-b", help="Short project description"),
35
+ ):
36
+ """Create a new empty project."""
37
+ client = PodojoClient()
38
+ try:
39
+ result = client.create_project(name, brief)
40
+ except httpx.HTTPStatusError as e:
41
+ if e.response.status_code == 409:
42
+ console.print(f"[red]Project '{name}' already exists.[/red]")
43
+ raise typer.Exit(code=1)
44
+ console.print(f"[red]{e.response.status_code}: {e.response.text}[/red]")
45
+ raise typer.Exit(code=1)
46
+
47
+ console.print(f"Created project [bold]{result['name']}[/bold] (id={result['id']})")
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  from pathlib import Path
2
3
 
3
4
  import typer
@@ -10,6 +11,15 @@ app = typer.Typer(help="Manage transcripts")
10
11
  console = Console()
11
12
 
12
13
 
14
+ def _format_date(value) -> str:
15
+ if not value:
16
+ return ""
17
+ try:
18
+ return datetime.fromisoformat(value.replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M")
19
+ except (ValueError, AttributeError):
20
+ return str(value)
21
+
22
+
13
23
  @app.command("list")
14
24
  def list_transcripts(project: str = typer.Argument(help="Project name")):
15
25
  """List transcripts for a project."""
@@ -17,11 +27,12 @@ def list_transcripts(project: str = typer.Argument(help="Project name")):
17
27
  data = client.list_transcripts(project)
18
28
 
19
29
  table = Table(title=f"Transcripts — {project} ({data['total']} total)")
30
+ table.add_column("Date")
20
31
  table.add_column("Batch ID")
21
32
  table.add_column("Name")
22
33
 
23
34
  for t in data["interviews"]:
24
- table.add_row(t.get("batch_id", ""), t.get("batch_name", ""))
35
+ table.add_row(_format_date(t.get("date")), t.get("batch_id", ""), t.get("batch_name", ""))
25
36
 
26
37
  console.print(table)
27
38
 
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  from pathlib import Path
2
3
 
3
4
  import httpx
@@ -12,6 +13,17 @@ app = typer.Typer(help="Manage videos")
12
13
  console = Console()
13
14
 
14
15
 
16
+ def _format_date(value) -> str:
17
+ if not value:
18
+ return ""
19
+ if isinstance(value, datetime):
20
+ return value.strftime("%Y-%m-%d %H:%M")
21
+ try:
22
+ return datetime.fromisoformat(str(value).replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M")
23
+ except ValueError:
24
+ return str(value)
25
+
26
+
15
27
  @app.command("list")
16
28
  def list_videos(project: str = typer.Argument(help="Project name")):
17
29
  """List videos for a project."""
@@ -19,12 +31,14 @@ def list_videos(project: str = typer.Argument(help="Project name")):
19
31
  data = client.list_videos(project)
20
32
 
21
33
  table = Table(title=f"Videos — {project} ({data['total']} total)")
34
+ table.add_column("Date")
22
35
  table.add_column("Batch ID")
23
36
  table.add_column("Name")
24
37
  table.add_column("Duration (min)")
25
38
 
26
39
  for v in data["videos"]:
27
40
  table.add_row(
41
+ _format_date(v.get("batch_timestamp")),
28
42
  v.get("batch_id", ""),
29
43
  v.get("batch_name", ""),
30
44
  str(v.get("duration_minutes", "")),
@@ -1,6 +1,6 @@
1
1
  import typer
2
2
 
3
- from .commands import auth, gdrive, projects, usertests, showreel, transcripts, videos
3
+ from .commands import auth, gdrive, interviews, projects, showreel, transcripts, usertests, videos
4
4
 
5
5
  app = typer.Typer(
6
6
  name="podojo",
@@ -10,6 +10,7 @@ app = typer.Typer(
10
10
 
11
11
  app.add_typer(auth.app, name="auth")
12
12
  app.add_typer(projects.app, name="projects")
13
+ app.add_typer(interviews.app, name="interviews")
13
14
  app.add_typer(usertests.app, name="usertests")
14
15
  app.add_typer(transcripts.app, name="transcripts")
15
16
  app.add_typer(videos.app, name="videos")
@@ -0,0 +1,86 @@
1
+ from podojo_cli.main import app
2
+
3
+
4
+ def test_upload_interview(runner, httpx_mock, tmp_path):
5
+ httpx_mock.add_response(
6
+ method="POST",
7
+ url="http://test.local/api/v1/projects/Alpha/interviews",
8
+ json={
9
+ "batch_id": "batch-xyz",
10
+ "filename": "session_20260501120000.mp4",
11
+ "project_name": "Alpha",
12
+ "message": "File successfully uploaded",
13
+ },
14
+ )
15
+
16
+ file_path = tmp_path / "session.mp4"
17
+ file_path.write_bytes(b"fake video bytes")
18
+
19
+ result = runner.invoke(app, ["interviews", "upload", str(file_path), "--project", "Alpha"])
20
+
21
+ assert result.exit_code == 0
22
+ assert "session.mp4" in result.output
23
+ assert "batch-xyz" in result.output
24
+
25
+
26
+ def test_upload_interview_url_encodes_project(runner, httpx_mock, tmp_path):
27
+ httpx_mock.add_response(
28
+ method="POST",
29
+ url="http://test.local/api/v1/projects/Acme%20Q4/interviews",
30
+ json={
31
+ "batch_id": "batch-1",
32
+ "filename": "x.mp4",
33
+ "project_name": "Acme Q4",
34
+ "message": "ok",
35
+ },
36
+ )
37
+
38
+ file_path = tmp_path / "x.mp4"
39
+ file_path.write_bytes(b"data")
40
+
41
+ result = runner.invoke(app, ["interviews", "upload", str(file_path), "--project", "Acme Q4"])
42
+
43
+ assert result.exit_code == 0
44
+
45
+
46
+ def test_upload_interview_unknown_project(runner, httpx_mock, tmp_path):
47
+ httpx_mock.add_response(
48
+ method="POST",
49
+ url="http://test.local/api/v1/projects/Ghost/interviews",
50
+ status_code=404,
51
+ json={"detail": "Project 'Ghost' not found"},
52
+ )
53
+
54
+ file_path = tmp_path / "session.mp4"
55
+ file_path.write_bytes(b"fake")
56
+
57
+ result = runner.invoke(app, ["interviews", "upload", str(file_path), "--project", "Ghost"])
58
+
59
+ assert result.exit_code == 1
60
+ assert "not found" in result.output
61
+ assert "projects create" in result.output
62
+
63
+
64
+ def test_upload_interview_bad_extension(runner, httpx_mock, tmp_path):
65
+ httpx_mock.add_response(
66
+ method="POST",
67
+ url="http://test.local/api/v1/projects/Alpha/interviews",
68
+ status_code=400,
69
+ json={"detail": "File type not allowed"},
70
+ )
71
+
72
+ file_path = tmp_path / "notes.txt"
73
+ file_path.write_text("hello")
74
+
75
+ result = runner.invoke(app, ["interviews", "upload", str(file_path), "--project", "Alpha"])
76
+
77
+ assert result.exit_code == 1
78
+ assert "File type not allowed" in result.output
79
+
80
+
81
+ def test_upload_interview_missing_file(runner, tmp_path):
82
+ missing = tmp_path / "does-not-exist.mp4"
83
+
84
+ result = runner.invoke(app, ["interviews", "upload", str(missing), "--project", "Alpha"])
85
+
86
+ assert result.exit_code != 0
@@ -0,0 +1,72 @@
1
+ from podojo_cli.main import app
2
+
3
+
4
+ def test_list_projects(runner, httpx_mock):
5
+ httpx_mock.add_response(
6
+ url="http://test.local/api/v1/projects?skip=0&limit=50",
7
+ json={
8
+ "projects": [
9
+ {"name": "Alpha", "brief": "First project"},
10
+ {"name": "Beta", "brief": "Second project"},
11
+ ],
12
+ "total": 2,
13
+ "skip": 0,
14
+ "limit": 50,
15
+ },
16
+ )
17
+
18
+ result = runner.invoke(app, ["projects", "list"])
19
+
20
+ assert result.exit_code == 0
21
+ assert "Alpha" in result.output
22
+ assert "Beta" in result.output
23
+
24
+
25
+ def test_list_projects_empty(runner, httpx_mock):
26
+ httpx_mock.add_response(
27
+ url="http://test.local/api/v1/projects?skip=0&limit=50",
28
+ json={
29
+ "projects": [],
30
+ "total": 0,
31
+ "skip": 0,
32
+ "limit": 50,
33
+ },
34
+ )
35
+
36
+ result = runner.invoke(app, ["projects", "list"])
37
+
38
+ assert result.exit_code == 0
39
+
40
+
41
+ def test_create_project(runner, httpx_mock):
42
+ httpx_mock.add_response(
43
+ method="POST",
44
+ url="http://test.local/api/v1/projects",
45
+ match_json={"name": "Gamma", "brief": "Third project"},
46
+ json={
47
+ "id": "abc123",
48
+ "name": "Gamma",
49
+ "brief": "Third project",
50
+ "message": "Project created successfully",
51
+ },
52
+ )
53
+
54
+ result = runner.invoke(app, ["projects", "create", "Gamma", "--brief", "Third project"])
55
+
56
+ assert result.exit_code == 0
57
+ assert "Gamma" in result.output
58
+ assert "abc123" in result.output
59
+
60
+
61
+ def test_create_project_duplicate(runner, httpx_mock):
62
+ httpx_mock.add_response(
63
+ method="POST",
64
+ url="http://test.local/api/v1/projects",
65
+ status_code=409,
66
+ json={"detail": "Project with this name already exists"},
67
+ )
68
+
69
+ result = runner.invoke(app, ["projects", "create", "Gamma"])
70
+
71
+ assert result.exit_code == 1
72
+ assert "already exists" in result.output
@@ -472,7 +472,7 @@ wheels = [
472
472
 
473
473
  [[package]]
474
474
  name = "podojo-cli"
475
- version = "0.4.4"
475
+ version = "0.5.0"
476
476
  source = { editable = "." }
477
477
  dependencies = [
478
478
  { name = "google-api-python-client" },
@@ -1,27 +0,0 @@
1
- import typer
2
- from rich.console import Console
3
- from rich.table import Table
4
-
5
- from ..client import PodojoClient
6
-
7
- app = typer.Typer(help="Manage projects")
8
- console = Console()
9
-
10
-
11
- @app.command("list")
12
- def list_projects(
13
- skip: int = typer.Option(0, help="Number of projects to skip"),
14
- limit: int = typer.Option(50, help="Max projects to return"),
15
- ):
16
- """List all projects."""
17
- client = PodojoClient()
18
- projects = client.list_projects(skip=skip, limit=limit)
19
-
20
- table = Table(title="Projects")
21
- table.add_column("Name")
22
- table.add_column("Brief")
23
-
24
- for p in projects:
25
- table.add_row(p.get("name", ""), p.get("brief", ""))
26
-
27
- console.print(table)
@@ -1,38 +0,0 @@
1
- from podojo_cli.main import app
2
-
3
-
4
- def test_list_projects(runner, httpx_mock):
5
- httpx_mock.add_response(
6
- url="http://test.local/api/v1/projects?skip=0&limit=50",
7
- json={
8
- "projects": [
9
- {"name": "Alpha", "brief": "First project"},
10
- {"name": "Beta", "brief": "Second project"},
11
- ],
12
- "total": 2,
13
- "skip": 0,
14
- "limit": 50,
15
- },
16
- )
17
-
18
- result = runner.invoke(app, ["projects", "list"])
19
-
20
- assert result.exit_code == 0
21
- assert "Alpha" in result.output
22
- assert "Beta" in result.output
23
-
24
-
25
- def test_list_projects_empty(runner, httpx_mock):
26
- httpx_mock.add_response(
27
- url="http://test.local/api/v1/projects?skip=0&limit=50",
28
- json={
29
- "projects": [],
30
- "total": 0,
31
- "skip": 0,
32
- "limit": 50,
33
- },
34
- )
35
-
36
- result = runner.invoke(app, ["projects", "list"])
37
-
38
- assert result.exit_code == 0
File without changes
File without changes
File without changes
File without changes