podojo-cli 1.25.0__tar.gz → 1.25.2__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-1.25.0 → podojo_cli-1.25.2}/CHANGELOG.md +26 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/PKG-INFO +2 -2
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/README.md +1 -1
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/pyproject.toml +1 -1
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/aiinterviews.py +9 -6
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/usertests.py +40 -20
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_aiinterviews.py +15 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_usertests.py +25 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/uv.lock +1 -1
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/.github/workflows/publish.yml +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/.gitignore +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/CLAUDE.md +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/LICENSE +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/__init__.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/client.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/__init__.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/auth.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/interviews.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/projects.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/showreel.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/synth.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/transcripts.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/commands/videos.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/config.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/main.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/synth/__init__.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/synth/driver.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/synth/session.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/version_check.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/video/__init__.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/src/podojo_cli/video/showreel.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/conftest.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_auth.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_interviews.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_projects.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_showreel.py +0 -0
- {podojo_cli-1.25.0 → podojo_cli-1.25.2}/tests/test_transcripts.py +0 -0
|
@@ -5,6 +5,32 @@ 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
|
+
## [1.25.2] - 2026-10-01
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- `usertests snippet` prints the recording snippet as two script tags plus a
|
|
12
|
+
`recorder.js` file to save next to the pages. The `rrweb.record()` call used
|
|
13
|
+
to be an inline `<script>`, which Boltable's content security policy
|
|
14
|
+
refuses, so recording never started on Boltable prototypes. The hints after
|
|
15
|
+
`usertests create` and on a failed go-live check now say to add the snippet
|
|
16
|
+
to every page instead of the `<head>`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `usertests get` and `aiinterviews get` print plain YAML, so
|
|
20
|
+
`get > file.yaml` can be edited and pushed back with `update -f`. The
|
|
21
|
+
output used to be hard-wrapped at the terminal width (or 80 columns when
|
|
22
|
+
redirected), which split long links and broke the YAML, and `[...]` text
|
|
23
|
+
was treated as console markup. The response count and preview/live links
|
|
24
|
+
now go to stderr, so they no longer end up in the saved file.
|
|
25
|
+
|
|
26
|
+
## [1.25.1] - 2026-09-24
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- YAML files for AI interviews and usertests are now read as UTF-8
|
|
30
|
+
explicitly. Reading them with the machine's locale encoding double-
|
|
31
|
+
encoded multi-byte characters (em-dashes, arrows) into mojibake when
|
|
32
|
+
uploading from a Windows machine.
|
|
33
|
+
|
|
8
34
|
## [1.25.0] - 2026-09-20
|
|
9
35
|
|
|
10
36
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: podojo-cli
|
|
3
|
-
Version: 1.25.
|
|
3
|
+
Version: 1.25.2
|
|
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
|
|
@@ -116,7 +116,7 @@ podojo usertests list
|
|
|
116
116
|
podojo usertests get checkout-usability-v1
|
|
117
117
|
podojo usertests update checkout-usability-v1 -f changes.yaml
|
|
118
118
|
podojo usertests delete checkout-usability-v1
|
|
119
|
-
podojo usertests snippet #
|
|
119
|
+
podojo usertests snippet # recording snippet: two script tags + recorder.js
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
A user test can open with an optional participant screener: on-screen
|
|
@@ -84,7 +84,7 @@ podojo usertests list
|
|
|
84
84
|
podojo usertests get checkout-usability-v1
|
|
85
85
|
podojo usertests update checkout-usability-v1 -f changes.yaml
|
|
86
86
|
podojo usertests delete checkout-usability-v1
|
|
87
|
-
podojo usertests snippet #
|
|
87
|
+
podojo usertests snippet # recording snippet: two script tags + recorder.js
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
A user test can open with an optional participant screener: on-screen
|
|
@@ -11,6 +11,7 @@ from ..config import load_config
|
|
|
11
11
|
|
|
12
12
|
app = typer.Typer(help="Manage AI voice interviews")
|
|
13
13
|
console = Console()
|
|
14
|
+
err_console = Console(stderr=True, soft_wrap=True)
|
|
14
15
|
|
|
15
16
|
REQUIRED_FIELDS = ["interview_id", "title", "questions"]
|
|
16
17
|
REQUIRED_QUESTION_FIELDS = ["text"]
|
|
@@ -324,7 +325,7 @@ def _load_yaml(path: Path) -> dict:
|
|
|
324
325
|
console.print(f"[red]Error:[/red] File not found: {path}")
|
|
325
326
|
raise typer.Exit(1)
|
|
326
327
|
try:
|
|
327
|
-
with open(path) as f:
|
|
328
|
+
with open(path, encoding="utf-8") as f:
|
|
328
329
|
data = yaml.safe_load(f)
|
|
329
330
|
except yaml.YAMLError as e:
|
|
330
331
|
console.print(f"[red]Error:[/red] Invalid YAML syntax:\n{e}")
|
|
@@ -449,16 +450,18 @@ def get_ai_interview(
|
|
|
449
450
|
for key in ("id", "created_at", "created_by", "last_updated"):
|
|
450
451
|
interview.pop(key, None)
|
|
451
452
|
|
|
452
|
-
|
|
453
|
+
# Plain stdout, so `get > file.yaml` round-trips: rich would hard-wrap long
|
|
454
|
+
# lines and eat [markup]. The info lines go to stderr for the same reason.
|
|
455
|
+
typer.echo(yaml.dump(interview, default_flow_style=False, sort_keys=False, allow_unicode=True))
|
|
453
456
|
max_responses = interview.get("max_responses")
|
|
454
457
|
if max_responses:
|
|
455
|
-
|
|
458
|
+
err_console.print(f"Responses: {response_count} / {max_responses}")
|
|
456
459
|
elif response_count:
|
|
457
|
-
|
|
460
|
+
err_console.print(f"Responses: {response_count}")
|
|
458
461
|
if group:
|
|
459
462
|
base = _ai_interviews_url()
|
|
460
|
-
|
|
461
|
-
|
|
463
|
+
err_console.print(f"Preview: {base}/preview/{group}/{interview_id}")
|
|
464
|
+
err_console.print(f"Live: {base}/{group}/{interview_id}")
|
|
462
465
|
|
|
463
466
|
|
|
464
467
|
@app.command("create")
|
|
@@ -10,26 +10,44 @@ from ..client import PodojoClient
|
|
|
10
10
|
|
|
11
11
|
app = typer.Typer(help="Manage unmoderated user tests")
|
|
12
12
|
console = Console()
|
|
13
|
+
err_console = Console(stderr=True, soft_wrap=True)
|
|
13
14
|
|
|
14
15
|
REQUIRED_FIELDS = ["usertest_id", "title", "prototype_url", "steps"]
|
|
15
16
|
VALID_STEP_TYPES = {"screen", "prototype"}
|
|
16
17
|
VALID_STEP_VARIANTS = {"question", "task", "instruction"}
|
|
17
18
|
REQUIRED_STEP_FIELDS = ["type", "title"]
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
# The rrweb.record() call lives in recorder.js rather than an inline <script>:
|
|
21
|
+
# Boltable's CSP refuses inline scripts, so an inline call never runs there.
|
|
22
|
+
SNIPPET_TAGS = """\
|
|
20
23
|
<script src="https://usertests.podojo.com/player-v1.js" integrity="sha384-UpKcjeLZtEcYxELLISURK3MAcN3KQdTx8iygewcEWVl8LqgVrftgiMtuMHod7D8e" crossorigin="anonymous"></script>
|
|
21
|
-
<script>
|
|
24
|
+
<script src="./recorder.js"></script>"""
|
|
25
|
+
|
|
26
|
+
RECORDER_JS = """\
|
|
27
|
+
(function initRecorder() {
|
|
22
28
|
if (typeof rrweb === "undefined") {
|
|
23
29
|
console.error("[rrweb] failed to load — screen recording disabled");
|
|
24
|
-
|
|
25
|
-
rrweb.record({
|
|
26
|
-
emit(event) {
|
|
27
|
-
window.parent.postMessage({ type: "rrweb-event", event: event }, "*");
|
|
28
|
-
},
|
|
29
|
-
checkoutEveryNms: 30000,
|
|
30
|
-
});
|
|
30
|
+
return;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
rrweb.record({
|
|
34
|
+
emit(event) {
|
|
35
|
+
window.parent.postMessage({ type: "rrweb-event", event: event }, "*");
|
|
36
|
+
},
|
|
37
|
+
checkoutEveryNms: 30000,
|
|
38
|
+
});
|
|
39
|
+
})();"""
|
|
40
|
+
|
|
41
|
+
RECORDER_SNIPPET = f"""\
|
|
42
|
+
1. Add these two lines to every HTML page of your prototype, just before </body>:
|
|
43
|
+
|
|
44
|
+
{SNIPPET_TAGS}
|
|
45
|
+
|
|
46
|
+
2. Save this as recorder.js in the same folder as your pages:
|
|
47
|
+
|
|
48
|
+
{RECORDER_JS}
|
|
49
|
+
|
|
50
|
+
Copy both exactly – any change stops the recording.
|
|
33
51
|
"""
|
|
34
52
|
|
|
35
53
|
EXAMPLE_YAML = """\
|
|
@@ -268,7 +286,7 @@ def _load_yaml(path: Path) -> dict:
|
|
|
268
286
|
console.print(f"[red]Error:[/red] File not found: {path}")
|
|
269
287
|
raise typer.Exit(1)
|
|
270
288
|
try:
|
|
271
|
-
with open(path) as f:
|
|
289
|
+
with open(path, encoding="utf-8") as f:
|
|
272
290
|
data = yaml.safe_load(f)
|
|
273
291
|
except yaml.YAMLError as e:
|
|
274
292
|
console.print(f"[red]Error:[/red] Invalid YAML syntax:\n{e}")
|
|
@@ -320,7 +338,7 @@ def _print_snippet_gate_error(e: httpx.HTTPStatusError) -> None:
|
|
|
320
338
|
reason = f"the recording snippet was not found at {url}"
|
|
321
339
|
console.print(f"[red]Error:[/red] Not going live: {reason}.")
|
|
322
340
|
console.print("Without the snippet, participant screens are not recorded.")
|
|
323
|
-
console.print(" - Add the snippet to your prototype
|
|
341
|
+
console.print(" - Add the snippet to every page of your prototype: [bold]podojo usertests snippet[/bold]")
|
|
324
342
|
console.print(" - or set [bold]live: false[/bold] to save a draft")
|
|
325
343
|
console.print(" - or re-run with [bold]--allow-missing-snippet[/bold] to go live without screen recording")
|
|
326
344
|
|
|
@@ -404,15 +422,17 @@ def get_usertest(
|
|
|
404
422
|
for key in ("id", "created_at", "created_by", "last_updated"):
|
|
405
423
|
usertest.pop(key, None)
|
|
406
424
|
|
|
407
|
-
|
|
425
|
+
# Plain stdout, so `get > file.yaml` round-trips: rich would hard-wrap long
|
|
426
|
+
# lines and eat [markup]. The info lines go to stderr for the same reason.
|
|
427
|
+
typer.echo(yaml.dump(usertest, default_flow_style=False, sort_keys=False, allow_unicode=True))
|
|
408
428
|
max_responses = usertest.get("max_responses")
|
|
409
429
|
if max_responses:
|
|
410
|
-
|
|
430
|
+
err_console.print(f"Responses: {response_count} / {max_responses}")
|
|
411
431
|
elif response_count:
|
|
412
|
-
|
|
432
|
+
err_console.print(f"Responses: {response_count}")
|
|
413
433
|
if group:
|
|
414
|
-
|
|
415
|
-
|
|
434
|
+
err_console.print(f"Preview: https://usertests.podojo.com/preview/{group}/{usertest_id}")
|
|
435
|
+
err_console.print(f"Live: https://usertests.podojo.com/{group}?test={usertest_id}")
|
|
416
436
|
|
|
417
437
|
|
|
418
438
|
@app.command("create")
|
|
@@ -457,8 +477,8 @@ def create_usertest(
|
|
|
457
477
|
console.print(f" Preview: https://usertests.podojo.com/preview/{group}/{usertest_id}")
|
|
458
478
|
console.print(f" Live: https://usertests.podojo.com/{group}?test={usertest_id}")
|
|
459
479
|
console.print(
|
|
460
|
-
"\n[dim]
|
|
461
|
-
"
|
|
480
|
+
"\n[dim]To record participants' screens, every page of your prototype needs "
|
|
481
|
+
"the recording snippet.\n"
|
|
462
482
|
"Run [bold]podojo usertests snippet[/bold] to print it.[/dim]"
|
|
463
483
|
)
|
|
464
484
|
|
|
@@ -539,5 +559,5 @@ def example():
|
|
|
539
559
|
|
|
540
560
|
@app.command("snippet")
|
|
541
561
|
def snippet():
|
|
542
|
-
"""Print the
|
|
562
|
+
"""Print the recording snippet: two script tags for every page, plus recorder.js."""
|
|
543
563
|
print(RECORDER_SNIPPET)
|
|
@@ -119,6 +119,21 @@ def test_get_ai_interview(runner, httpx_mock):
|
|
|
119
119
|
assert "http://interviews.test.local/test-group/ai-1" in result.output
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
def test_get_ai_interview_stdout_round_trips(runner, httpx_mock):
|
|
123
|
+
overview = "Participants [drivers in Tallinn] talk about " + "their last week of rides " * 6
|
|
124
|
+
httpx_mock.add_response(
|
|
125
|
+
url="http://test.local/api/v1/ai-interviews/ai-1",
|
|
126
|
+
json={**GET_RESPONSE, "overview": overview},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
result = runner.invoke(app, ["aiinterviews", "get", "ai-1"])
|
|
130
|
+
|
|
131
|
+
data = yaml.safe_load(result.stdout)
|
|
132
|
+
assert data["overview"] == overview
|
|
133
|
+
assert "Responses" not in data and "Preview" not in data and "Live" not in data
|
|
134
|
+
assert "Responses: 7 / 20" in result.stderr
|
|
135
|
+
|
|
136
|
+
|
|
122
137
|
def test_get_ai_interview_not_found(runner, httpx_mock):
|
|
123
138
|
httpx_mock.add_response(
|
|
124
139
|
url="http://test.local/api/v1/ai-interviews/nonexistent",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
|
|
1
3
|
from podojo_cli.main import app
|
|
2
4
|
|
|
3
5
|
|
|
@@ -107,6 +109,26 @@ def test_get_usertest(runner, httpx_mock):
|
|
|
107
109
|
assert "https://usertests.podojo.com/test-group?test=ut-1" in result.output
|
|
108
110
|
|
|
109
111
|
|
|
112
|
+
def test_get_usertest_stdout_round_trips(runner, httpx_mock):
|
|
113
|
+
long_url = "https://uxr-prototypes.static.boltable.eu/_t/y2eb-2jft-ekzv-cn53/price-lock-test/1.html"
|
|
114
|
+
httpx_mock.add_response(
|
|
115
|
+
url="http://test.local/api/v1/usertests/ut-1",
|
|
116
|
+
json={
|
|
117
|
+
**GET_RESPONSE,
|
|
118
|
+
"prototype_url": long_url,
|
|
119
|
+
"welcome_text": "Tap [Continue] when you are ready",
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
result = runner.invoke(app, ["usertests", "get", "ut-1"])
|
|
124
|
+
|
|
125
|
+
data = yaml.safe_load(result.stdout)
|
|
126
|
+
assert data["prototype_url"] == long_url
|
|
127
|
+
assert data["welcome_text"] == "Tap [Continue] when you are ready"
|
|
128
|
+
assert "Responses" not in data and "Preview" not in data and "Live" not in data
|
|
129
|
+
assert "Preview: https://usertests.podojo.com/preview/test-group/ut-1" in result.stderr
|
|
130
|
+
|
|
131
|
+
|
|
110
132
|
def test_get_usertest_not_found(runner, httpx_mock):
|
|
111
133
|
httpx_mock.add_response(
|
|
112
134
|
url="http://test.local/api/v1/usertests/nonexistent",
|
|
@@ -615,4 +637,7 @@ def test_snippet(runner):
|
|
|
615
637
|
assert result.exit_code == 0
|
|
616
638
|
assert "player-v1.js" in result.output
|
|
617
639
|
assert 'integrity="sha384-' in result.output
|
|
640
|
+
assert '<script src="./recorder.js"></script>' in result.output
|
|
618
641
|
assert "rrweb.record" in result.output
|
|
642
|
+
# the recording must start from recorder.js: Boltable refuses inline scripts
|
|
643
|
+
assert "<script>" not in result.output
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|