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.
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/CHANGELOG.md +11 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/PKG-INFO +1 -1
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/pyproject.toml +1 -1
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/client.py +34 -0
- podojo_cli-0.5.0/src/podojo_cli/commands/interviews.py +57 -0
- podojo_cli-0.5.0/src/podojo_cli/commands/projects.py +47 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/transcripts.py +12 -1
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/videos.py +14 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/main.py +2 -1
- podojo_cli-0.5.0/tests/test_interviews.py +86 -0
- podojo_cli-0.5.0/tests/test_projects.py +72 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/uv.lock +1 -1
- podojo_cli-0.4.4/src/podojo_cli/commands/projects.py +0 -27
- podojo_cli-0.4.4/tests/test_projects.py +0 -38
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/.github/workflows/publish.yml +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/.gitignore +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/CLAUDE.md +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/README.md +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/__init__.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/__init__.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/auth.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/gdrive.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/showreel.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/commands/usertests.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/config.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/gdrive/__init__.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/gdrive/list.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/gdrive/upload.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/video/__init__.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/src/podojo_cli/video/showreel.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/tests/conftest.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/tests/test_auth.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/tests/test_gdrive.py +0 -0
- {podojo_cli-0.4.4 → podojo_cli-0.5.0}/tests/test_showreel.py +0 -0
- {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,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,
|
|
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
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|