podojo-cli 1.25.2__tar.gz → 1.26.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 (37) hide show
  1. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/CHANGELOG.md +23 -0
  2. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/PKG-INFO +8 -6
  3. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/README.md +7 -5
  4. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/pyproject.toml +1 -1
  5. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/commands/showreel.py +1 -1
  6. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/commands/transcripts.py +1 -1
  7. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/commands/usertests.py +25 -13
  8. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/config.py +3 -3
  9. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/video/showreel.py +1 -1
  10. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/tests/test_usertests.py +59 -1
  11. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/uv.lock +1 -1
  12. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/.github/workflows/publish.yml +0 -0
  13. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/.gitignore +0 -0
  14. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/CLAUDE.md +0 -0
  15. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/LICENSE +0 -0
  16. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/__init__.py +0 -0
  17. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/client.py +0 -0
  18. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/commands/__init__.py +0 -0
  19. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/commands/aiinterviews.py +0 -0
  20. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/commands/auth.py +0 -0
  21. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/commands/interviews.py +0 -0
  22. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/commands/projects.py +0 -0
  23. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/commands/synth.py +0 -0
  24. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/commands/videos.py +0 -0
  25. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/main.py +0 -0
  26. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/synth/__init__.py +0 -0
  27. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/synth/driver.py +0 -0
  28. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/synth/session.py +0 -0
  29. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/version_check.py +0 -0
  30. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/src/podojo_cli/video/__init__.py +0 -0
  31. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/tests/conftest.py +0 -0
  32. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/tests/test_aiinterviews.py +0 -0
  33. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/tests/test_auth.py +0 -0
  34. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/tests/test_interviews.py +0 -0
  35. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/tests/test_projects.py +0 -0
  36. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/tests/test_showreel.py +0 -0
  37. {podojo_cli-1.25.2 → podojo_cli-1.26.0}/tests/test_transcripts.py +0 -0
@@ -5,6 +5,29 @@ 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.26.0] - 2026-10-08
9
+
10
+ ### Changed
11
+ - User test `screening_questions` no longer screen by default, matching AI
12
+ interviews: a question only gates entry when it carries `screener: true`
13
+ (then at least one option needs `qualifies: true`, as before). Questions
14
+ without the flag are plain closed questions — every answer is accepted and
15
+ recorded, and `qualifies` flags are not needed. Mixed `qualifies` flags
16
+ without `screener: true` are rejected by `validate`/`create`/`update` so a
17
+ test can't silently stop screening: add `screener: true` to existing
18
+ screener questions in your YAML files. Tests already stored in Podojo keep
19
+ screening as before.
20
+
21
+ ## [1.25.3] - 2026-10-02
22
+
23
+ ### Fixed
24
+ - The remaining file reads and writes use UTF-8 explicitly instead of the
25
+ machine's locale encoding, finishing the Windows (cp1252) fix from 1.25.1:
26
+ `transcripts download --output` no longer fails on or mis-encodes
27
+ non-ASCII transcripts, `showreel create` reads the clips JSON as UTF-8 and
28
+ writes ffmpeg's concat list as UTF-8 (paths with umlauts broke it), and
29
+ `~/.podojo.toml` is read and written as UTF-8.
30
+
8
31
  ## [1.25.2] - 2026-10-01
9
32
 
10
33
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: podojo-cli
3
- Version: 1.25.2
3
+ Version: 1.26.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
@@ -119,11 +119,13 @@ podojo usertests delete checkout-usability-v1
119
119
  podojo usertests snippet # recording snippet: two script tags + recorder.js
120
120
  ```
121
121
 
122
- A user test can open with an optional participant screener: on-screen
123
- single-select `screening_questions` whose options carry `qualifies: true`
124
- flags. Participants must pick a qualifying option on every question; everyone
125
- else sees the test's `rejection_message` and never reaches the recorded test.
126
- See `podojo usertests example` for the exact shape.
122
+ A user test can open with optional on-screen `screening_questions` (closed
123
+ questions, single- or multi-select), answered before consent and recording;
124
+ answers are recorded with each session. A question marked `screener: true`
125
+ also gates entry: its options carry `qualifies: true` flags, and participants
126
+ who don't pick a qualifying option see the test's `rejection_message` and
127
+ never reach the recorded test. See `podojo usertests example` for the exact
128
+ shape.
127
129
 
128
130
  Set `required_device: mobile` or `required_device: desktop` to restrict a test
129
131
  to one kind of device. Participants who open the link on the other kind are
@@ -87,11 +87,13 @@ podojo usertests delete checkout-usability-v1
87
87
  podojo usertests snippet # recording snippet: two script tags + recorder.js
88
88
  ```
89
89
 
90
- A user test can open with an optional participant screener: on-screen
91
- single-select `screening_questions` whose options carry `qualifies: true`
92
- flags. Participants must pick a qualifying option on every question; everyone
93
- else sees the test's `rejection_message` and never reaches the recorded test.
94
- See `podojo usertests example` for the exact shape.
90
+ A user test can open with optional on-screen `screening_questions` (closed
91
+ questions, single- or multi-select), answered before consent and recording;
92
+ answers are recorded with each session. A question marked `screener: true`
93
+ also gates entry: its options carry `qualifies: true` flags, and participants
94
+ who don't pick a qualifying option see the test's `rejection_message` and
95
+ never reach the recorded test. See `podojo usertests example` for the exact
96
+ shape.
95
97
 
96
98
  Set `required_device: mobile` or `required_device: desktop` to restrict a test
97
99
  to one kind of device. Participants who open the link on the other kind are
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "podojo-cli"
3
- version = "1.25.2"
3
+ version = "1.26.0"
4
4
  description = "CLI for the Podojo user research platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -56,7 +56,7 @@ def create_showreel(
56
56
  """
57
57
  _check_ffmpeg()
58
58
 
59
- with open(clips_json) as f:
59
+ with open(clips_json, encoding="utf-8") as f:
60
60
  clips = json.load(f)
61
61
 
62
62
  if not clips:
@@ -52,7 +52,7 @@ def download_transcript(
52
52
  text = client.download_transcript(project, batch_id)
53
53
 
54
54
  if output:
55
- output.write_text(text)
55
+ output.write_text(text, encoding="utf-8")
56
56
  console.print(f"Saved to {output}")
57
57
  else:
58
58
  console.print(text)
@@ -58,15 +58,17 @@ EXAMPLE_YAML = """\
58
58
  # screening_questions, rejection_message, project_name,
59
59
  # live, collect_contact, max_responses, required_device
60
60
  #
61
- # Each screening question (optional participant screener, answered on screen
62
- # before consent — screening is never recorded):
61
+ # Each screening question (optional closed questions, answered on screen
62
+ # before consent and recording; answers are stored with the session):
63
63
  # text (required) multiple-choice question
64
64
  # multi_select (optional, default false) participants can pick several
65
- # options; one qualifying pick passes the question
66
- # options (required, at least 2) each with `text` and an optional
67
- # `qualifies: true` — participants must pick a qualifying
68
- # option on every question, otherwise they see the
69
- # rejection_message and the test never starts
65
+ # options
66
+ # screener (optional, default false) the question screens: participants
67
+ # must pick an option with `qualifies: true`, otherwise they
68
+ # see the rejection_message and the test never starts
69
+ # options (required, at least 2) each with `text`; `qualifies: true`
70
+ # marks the accepted answers of a screener question and is
71
+ # ignored on plain closed questions
70
72
 
71
73
  usertest_id: checkout-usability-v1
72
74
  title: Checkout Flow Usability Test
@@ -113,20 +115,20 @@ project_name: checkout-redesign-q1
113
115
  # never start a screener or use up a session.
114
116
  # required_device: mobile
115
117
 
116
- # Optional: participant screener — shown on screen before consent and
117
- # recording. Only qualified participants reach the test; screen-out answers
118
- # are still captured for funnel metrics.
118
+ # Optional: closed questions — shown on screen before consent and recording.
119
+ # Answers are captured with the session; only questions marked
120
+ # `screener: true` can screen participants out. Screen-out answers are still
121
+ # captured for funnel metrics.
119
122
  screening_questions:
120
123
  - text: How often do you shop online?
121
124
  options:
122
125
  - text: Rarely or never
123
126
  - text: A few times a year
124
127
  - text: At least once a month
125
- qualifies: true
126
128
  - text: Weekly or more
127
- qualifies: true
128
129
 
129
130
  - text: Have you abandoned an online purchase at checkout in the past 3 months?
131
+ screener: true
130
132
  options:
131
133
  - text: "Yes"
132
134
  qualifies: true
@@ -135,6 +137,7 @@ screening_questions:
135
137
 
136
138
  - text: Which devices do you use to shop online?
137
139
  multi_select: true
140
+ screener: true
138
141
  options:
139
142
  - text: Phone
140
143
  qualifies: true
@@ -255,6 +258,10 @@ def validate_usertest_data(data: dict) -> list[str]:
255
258
  errors.append(
256
259
  f"Screening question {i}: 'multi_select' must be true or false"
257
260
  )
261
+ screener = question.get("screener", False)
262
+ if not isinstance(screener, bool):
263
+ errors.append(f"Screening question {i}: 'screener' must be true or false")
264
+ screener = False
258
265
  options = question.get("options")
259
266
  if not isinstance(options, list) or len(options) < 2:
260
267
  errors.append(f"Screening question {i}: 'options' must list at least 2 options")
@@ -273,10 +280,15 @@ def validate_usertest_data(data: dict) -> list[str]:
273
280
  )
274
281
  elif qualifies:
275
282
  qualifying += 1
276
- if qualifying == 0:
283
+ if screener and qualifying == 0:
277
284
  errors.append(
278
285
  f"Screening question {i}: needs at least one option with 'qualifies: true'"
279
286
  )
287
+ if not screener and 0 < qualifying < len(options):
288
+ errors.append(
289
+ f"Screening question {i}: has non-qualifying options but no "
290
+ "'screener: true' — set it, or drop the 'qualifies' flags"
291
+ )
280
292
  return errors
281
293
 
282
294
 
@@ -14,7 +14,7 @@ DEFAULT_AI_INTERVIEWS_URL = "https://ai-interviews.podojo.com"
14
14
  def load_config() -> dict:
15
15
  config = {}
16
16
  if CONFIG_PATH.exists():
17
- config = tomllib.loads(CONFIG_PATH.read_text())
17
+ config = tomllib.loads(CONFIG_PATH.read_text(encoding="utf-8"))
18
18
  config.setdefault("base_url", os.getenv("PODOJO_BASE_URL", DEFAULT_BASE_URL))
19
19
  config.setdefault(
20
20
  "ai_interviews_url", os.getenv("PODOJO_AI_INTERVIEWS_URL", DEFAULT_AI_INTERVIEWS_URL)
@@ -39,10 +39,10 @@ def clear_api_key():
39
39
  pass
40
40
  if not CONFIG_PATH.exists():
41
41
  return
42
- config = tomllib.loads(CONFIG_PATH.read_text())
42
+ config = tomllib.loads(CONFIG_PATH.read_text(encoding="utf-8"))
43
43
  config.pop("api_key", None)
44
44
  if config:
45
45
  lines = "\n".join(f'{k} = "{v}"' for k, v in config.items())
46
- CONFIG_PATH.write_text(lines + "\n")
46
+ CONFIG_PATH.write_text(lines + "\n", encoding="utf-8")
47
47
  else:
48
48
  CONFIG_PATH.unlink()
@@ -57,7 +57,7 @@ def make_title_card(participant: str, country: str, topic: str, dst: str):
57
57
 
58
58
  def concatenate(parts: list[str], output: str):
59
59
  concat_list = os.path.join(os.path.dirname(output), "_concat.txt")
60
- with open(concat_list, "w") as f:
60
+ with open(concat_list, "w", encoding="utf-8") as f:
61
61
  for p in parts:
62
62
  f.write(f"file '{os.path.abspath(p)}'\n")
63
63
 
@@ -509,12 +509,14 @@ def test_validate_required_device_rejects_other_values(runner, tmp_path):
509
509
  SCREENING_YAML = VALID_USERTEST_YAML + """\
510
510
  screening_questions:
511
511
  - text: Do you shop online?
512
+ screener: true
512
513
  options:
513
514
  - text: "Yes"
514
515
  qualifies: true
515
516
  - text: "No"
516
517
  - text: How often do you shop online?
517
518
  multi_select: true
519
+ screener: true
518
520
  options:
519
521
  - text: Never
520
522
  - text: Monthly
@@ -552,12 +554,13 @@ def test_validate_screening_question_needs_two_options(runner, tmp_path):
552
554
  assert "'options' must list at least 2 options" in result.output
553
555
 
554
556
 
555
- def test_validate_screening_question_needs_a_qualifying_option(runner, tmp_path):
557
+ def test_validate_screener_needs_a_qualifying_option(runner, tmp_path):
556
558
  yaml_file = tmp_path / "usertest.yaml"
557
559
  yaml_file.write_text(
558
560
  VALID_USERTEST_YAML
559
561
  + "screening_questions:\n"
560
562
  + " - text: Nobody can pass this one\n"
563
+ + " screener: true\n"
561
564
  + " options:\n"
562
565
  + " - text: A\n"
563
566
  + " - text: B\n"
@@ -569,6 +572,61 @@ def test_validate_screening_question_needs_a_qualifying_option(runner, tmp_path)
569
572
  assert "needs at least one option with 'qualifies: true'" in result.output
570
573
 
571
574
 
575
+ def test_validate_plain_closed_question_needs_no_qualifying_option(runner, tmp_path):
576
+ yaml_file = tmp_path / "usertest.yaml"
577
+ yaml_file.write_text(
578
+ VALID_USERTEST_YAML
579
+ + "screening_questions:\n"
580
+ + " - text: Which device do you use?\n"
581
+ + " options:\n"
582
+ + " - text: Phone\n"
583
+ + " - text: Laptop\n"
584
+ )
585
+
586
+ result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
587
+
588
+ assert result.exit_code == 0
589
+ assert "Valid user test config" in result.output
590
+
591
+
592
+ def test_validate_rejects_mixed_qualifies_without_screener_flag(runner, tmp_path):
593
+ # Non-qualifying options are screener intent; without the flag they would
594
+ # silently stop screening anyone out.
595
+ yaml_file = tmp_path / "usertest.yaml"
596
+ yaml_file.write_text(
597
+ VALID_USERTEST_YAML
598
+ + "screening_questions:\n"
599
+ + " - text: Do you shop online?\n"
600
+ + " options:\n"
601
+ + " - text: \"Yes\"\n"
602
+ + " qualifies: true\n"
603
+ + " - text: \"No\"\n"
604
+ )
605
+
606
+ result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
607
+
608
+ assert result.exit_code == 1
609
+ assert "has non-qualifying options but no 'screener: true'" in result.output
610
+
611
+
612
+ def test_validate_screening_screener_must_be_bool(runner, tmp_path):
613
+ yaml_file = tmp_path / "usertest.yaml"
614
+ yaml_file.write_text(
615
+ VALID_USERTEST_YAML
616
+ + "screening_questions:\n"
617
+ + " - text: Q\n"
618
+ + " screener: yep\n"
619
+ + " options:\n"
620
+ + " - text: A\n"
621
+ + " - text: B\n"
622
+ )
623
+
624
+ result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
625
+
626
+ assert result.exit_code == 1
627
+ assert "'screener' must be true or false" in result.output
628
+
629
+
572
630
  def test_validate_screening_multi_select_must_be_bool(runner, tmp_path):
573
631
  yaml_file = tmp_path / "usertest.yaml"
574
632
  yaml_file.write_text(
@@ -438,7 +438,7 @@ wheels = [
438
438
 
439
439
  [[package]]
440
440
  name = "podojo-cli"
441
- version = "1.25.2"
441
+ version = "1.26.0"
442
442
  source = { editable = "." }
443
443
  dependencies = [
444
444
  { name = "httpx" },
File without changes
File without changes
File without changes