podojo-cli 1.6.0__tar.gz → 1.7.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.6.0 → podojo_cli-1.7.0}/CHANGELOG.md +5 -0
  2. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/PKG-INFO +7 -1
  3. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/README.md +6 -0
  4. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/pyproject.toml +1 -1
  5. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/commands/usertests.py +66 -0
  6. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/tests/test_usertests.py +95 -0
  7. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/uv.lock +1 -1
  8. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/.github/workflows/publish.yml +0 -0
  9. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/.gitignore +0 -0
  10. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/CLAUDE.md +0 -0
  11. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/LICENSE +0 -0
  12. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/__init__.py +0 -0
  13. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/client.py +0 -0
  14. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/commands/__init__.py +0 -0
  15. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/commands/aiinterviews.py +0 -0
  16. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/commands/auth.py +0 -0
  17. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/commands/interviews.py +0 -0
  18. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/commands/projects.py +0 -0
  19. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/commands/showreel.py +0 -0
  20. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/commands/synth.py +0 -0
  21. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/commands/transcripts.py +0 -0
  22. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/commands/videos.py +0 -0
  23. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/config.py +0 -0
  24. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/main.py +0 -0
  25. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/synth/__init__.py +0 -0
  26. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/synth/driver.py +0 -0
  27. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/synth/session.py +0 -0
  28. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/version_check.py +0 -0
  29. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/video/__init__.py +0 -0
  30. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/src/podojo_cli/video/showreel.py +0 -0
  31. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/tests/conftest.py +0 -0
  32. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/tests/test_aiinterviews.py +0 -0
  33. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/tests/test_auth.py +0 -0
  34. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/tests/test_interviews.py +0 -0
  35. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/tests/test_projects.py +0 -0
  36. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/tests/test_showreel.py +0 -0
  37. {podojo_cli-1.6.0 → podojo_cli-1.7.0}/tests/test_transcripts.py +0 -0
@@ -5,6 +5,11 @@ 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.7.0] - 2026-07-04
9
+
10
+ ### Added
11
+ - User tests support an optional participant screener: `screening_questions` (single-select, options flagged `qualifies: true`) and `rejection_message`, mirroring AI interviews. `validate`/`create` check the screener shape, and `example` shows the new fields.
12
+
8
13
  ## [1.6.0] - 2026-07-03
9
14
 
10
15
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: podojo-cli
3
- Version: 1.6.0
3
+ Version: 1.7.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
@@ -123,6 +123,12 @@ podojo usertests delete checkout-usability-v1
123
123
  podojo usertests snippet # recorder script for self-hosted prototypes
124
124
  ```
125
125
 
126
+ A user test can open with an optional participant screener: on-screen
127
+ single-select `screening_questions` whose options carry `qualifies: true`
128
+ flags. Participants must pick a qualifying option on every question; everyone
129
+ else sees the test's `rejection_message` and never reaches the recorded test.
130
+ See `podojo usertests example` for the exact shape.
131
+
126
132
  ### AI interviews
127
133
 
128
134
  An AI interview is a self-serve voice conversation: an AI interviewer asks
@@ -92,6 +92,12 @@ podojo usertests delete checkout-usability-v1
92
92
  podojo usertests snippet # recorder script for self-hosted prototypes
93
93
  ```
94
94
 
95
+ A user test can open with an optional participant screener: on-screen
96
+ single-select `screening_questions` whose options carry `qualifies: true`
97
+ flags. Participants must pick a qualifying option on every question; everyone
98
+ else sees the test's `rejection_message` and never reaches the recorded test.
99
+ See `podojo usertests example` for the exact shape.
100
+
95
101
  ### AI interviews
96
102
 
97
103
  An AI interview is a self-serve voice conversation: an AI interviewer asks
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "podojo-cli"
3
- version = "1.6.0"
3
+ version = "1.7.0"
4
4
  description = "CLI for the Podojo user research platform"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -37,7 +37,16 @@ EXAMPLE_YAML = """\
37
37
  #
38
38
  # Required fields: usertest_id, title, logo, prototype_url, steps
39
39
  # Optional fields: welcome_text, promo_code, promo_code_info,
40
+ # screening_questions, rejection_message,
40
41
  # project_name, live, collect_contact
42
+ #
43
+ # Each screening question (optional participant screener, answered on screen
44
+ # before consent — screening is never recorded):
45
+ # text (required) single-select multiple-choice question
46
+ # options (required, at least 2) each with `text` and an optional
47
+ # `qualifies: true` — participants must pick a qualifying
48
+ # option on every question, otherwise they see the
49
+ # rejection_message and the test never starts
41
50
 
42
51
  usertest_id: checkout-usability-v1
43
52
  title: Checkout Flow Usability Test
@@ -66,6 +75,31 @@ project_name: checkout-redesign-q1
66
75
  # Optional: collect participant name/email on the final screen (default: false)
67
76
  # collect_contact: true
68
77
 
78
+ # Optional: participant screener — shown on screen before consent and
79
+ # recording. Only qualified participants reach the test; screen-out answers
80
+ # are still captured for funnel metrics.
81
+ screening_questions:
82
+ - text: How often do you shop online?
83
+ options:
84
+ - text: Rarely or never
85
+ - text: A few times a year
86
+ - text: At least once a month
87
+ qualifies: true
88
+ - text: Weekly or more
89
+ qualifies: true
90
+
91
+ - text: Have you abandoned an online purchase at checkout in the past 3 months?
92
+ options:
93
+ - text: "Yes"
94
+ qualifies: true
95
+ - text: "No"
96
+ - text: Not sure
97
+
98
+ # Optional: shown to participants whose screener answers don't qualify
99
+ rejection_message: >
100
+ Thank you for your time, you did not meet the research criteria for this
101
+ study!
102
+
69
103
  # Steps define what participants see and do
70
104
  # Each step requires: type ("screen" or "prototype") and title
71
105
  # Screen steps should have a variant: "question" (open-ended), "task" (action-based),
@@ -143,6 +177,38 @@ def validate_usertest_data(data: dict) -> list[str]:
143
177
  errors.append(
144
178
  f"Step {i}: 'variant' must be one of {sorted(VALID_STEP_VARIANTS)}, got '{variant}'"
145
179
  )
180
+
181
+ screening_questions = data.get("screening_questions")
182
+ if screening_questions is not None:
183
+ if not isinstance(screening_questions, list):
184
+ errors.append("'screening_questions' must be a list")
185
+ else:
186
+ for i, question in enumerate(screening_questions, 1):
187
+ if not isinstance(question, dict) or "text" not in question:
188
+ errors.append(f"Screening question {i}: must be a mapping with 'text'")
189
+ continue
190
+ options = question.get("options")
191
+ if not isinstance(options, list) or len(options) < 2:
192
+ errors.append(f"Screening question {i}: 'options' must list at least 2 options")
193
+ continue
194
+ qualifying = 0
195
+ for j, option in enumerate(options, 1):
196
+ if not isinstance(option, dict) or "text" not in option:
197
+ errors.append(
198
+ f"Screening question {i}, option {j}: must be a mapping with 'text'"
199
+ )
200
+ continue
201
+ qualifies = option.get("qualifies", False)
202
+ if not isinstance(qualifies, bool):
203
+ errors.append(
204
+ f"Screening question {i}, option {j}: 'qualifies' must be true or false"
205
+ )
206
+ elif qualifies:
207
+ qualifying += 1
208
+ if qualifying == 0:
209
+ errors.append(
210
+ f"Screening question {i}: needs at least one option with 'qualifies: true'"
211
+ )
146
212
  return errors
147
213
 
148
214
 
@@ -292,6 +292,87 @@ def test_validate_invalid(runner, tmp_path):
292
292
  assert "Missing required field" in result.output
293
293
 
294
294
 
295
+ SCREENING_YAML = VALID_USERTEST_YAML + """\
296
+ screening_questions:
297
+ - text: Do you shop online?
298
+ options:
299
+ - text: "Yes"
300
+ qualifies: true
301
+ - text: "No"
302
+ - text: How often do you shop online?
303
+ options:
304
+ - text: Never
305
+ - text: Monthly
306
+ qualifies: true
307
+ - text: Weekly
308
+ qualifies: true
309
+ rejection_message: You did not meet the research criteria for this study.
310
+ """
311
+
312
+
313
+ def test_validate_with_screening(runner, tmp_path):
314
+ yaml_file = tmp_path / "usertest.yaml"
315
+ yaml_file.write_text(SCREENING_YAML)
316
+
317
+ result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
318
+
319
+ assert result.exit_code == 0
320
+ assert "Valid user test config" in result.output
321
+
322
+
323
+ def test_validate_screening_question_needs_two_options(runner, tmp_path):
324
+ yaml_file = tmp_path / "usertest.yaml"
325
+ yaml_file.write_text(
326
+ VALID_USERTEST_YAML
327
+ + "screening_questions:\n"
328
+ + " - text: Only one way to answer?\n"
329
+ + " options:\n"
330
+ + " - text: \"Yes\"\n"
331
+ + " qualifies: true\n"
332
+ )
333
+
334
+ result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
335
+
336
+ assert result.exit_code == 1
337
+ assert "'options' must list at least 2 options" in result.output
338
+
339
+
340
+ def test_validate_screening_question_needs_a_qualifying_option(runner, tmp_path):
341
+ yaml_file = tmp_path / "usertest.yaml"
342
+ yaml_file.write_text(
343
+ VALID_USERTEST_YAML
344
+ + "screening_questions:\n"
345
+ + " - text: Nobody can pass this one\n"
346
+ + " options:\n"
347
+ + " - text: A\n"
348
+ + " - text: B\n"
349
+ )
350
+
351
+ result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
352
+
353
+ assert result.exit_code == 1
354
+ assert "needs at least one option with 'qualifies: true'" in result.output
355
+
356
+
357
+ def test_validate_screening_option_qualifies_must_be_bool(runner, tmp_path):
358
+ yaml_file = tmp_path / "usertest.yaml"
359
+ yaml_file.write_text(
360
+ VALID_USERTEST_YAML
361
+ + "screening_questions:\n"
362
+ + " - text: Q\n"
363
+ + " options:\n"
364
+ + " - text: A\n"
365
+ + " qualifies: yep\n"
366
+ + " - text: B\n"
367
+ + " qualifies: true\n"
368
+ )
369
+
370
+ result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
371
+
372
+ assert result.exit_code == 1
373
+ assert "'qualifies' must be true or false" in result.output
374
+
375
+
295
376
  def test_example(runner):
296
377
  result = runner.invoke(app, ["usertests", "example"])
297
378
 
@@ -300,6 +381,20 @@ def test_example(runner):
300
381
  assert "steps:" in result.output
301
382
  assert "prototype" in result.output
302
383
  assert "screen" in result.output
384
+ assert "screening_questions:" in result.output
385
+ assert "rejection_message:" in result.output
386
+
387
+
388
+ def test_example_validates(runner, tmp_path):
389
+ from podojo_cli.commands.usertests import EXAMPLE_YAML
390
+
391
+ yaml_file = tmp_path / "example.yaml"
392
+ yaml_file.write_text(EXAMPLE_YAML)
393
+
394
+ result = runner.invoke(app, ["usertests", "validate", str(yaml_file)])
395
+
396
+ assert result.exit_code == 0
397
+ assert "Valid user test config" in result.output
303
398
 
304
399
 
305
400
  def test_snippet(runner):
@@ -369,7 +369,7 @@ wheels = [
369
369
 
370
370
  [[package]]
371
371
  name = "podojo-cli"
372
- version = "1.6.0"
372
+ version = "1.7.0"
373
373
  source = { editable = "." }
374
374
  dependencies = [
375
375
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes