prettyplay 0.0.2__py3-none-any.whl

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 (75) hide show
  1. prettyplay/.usages/lifecycle.md +117 -0
  2. prettyplay/.usages/steps.md +169 -0
  3. prettyplay/__init__.py +18 -0
  4. prettyplay/cache/.usages/addressing.md +44 -0
  5. prettyplay/cache/.usages/budgets.md +51 -0
  6. prettyplay/cache/.usages/storage.md +32 -0
  7. prettyplay/cache/__init__.py +8 -0
  8. prettyplay/cache/budgets.py +93 -0
  9. prettyplay/cache/models.py +55 -0
  10. prettyplay/cache/store.py +276 -0
  11. prettyplay/cache/text.py +36 -0
  12. prettyplay/config/.usages/configuration.md +246 -0
  13. prettyplay/config/__init__.py +6 -0
  14. prettyplay/config/loader.py +336 -0
  15. prettyplay/config/models.py +288 -0
  16. prettyplay/driver/.usages/error_kinds.md +26 -0
  17. prettyplay/driver/.usages/plumbing.md +52 -0
  18. prettyplay/driver/__init__.py +11 -0
  19. prettyplay/driver/errors.py +76 -0
  20. prettyplay/driver/page.py +175 -0
  21. prettyplay/driver/session.py +330 -0
  22. prettyplay/engine/.usages/generation.md +157 -0
  23. prettyplay/engine/.usages/healing.md +61 -0
  24. prettyplay/engine/__init__.py +19 -0
  25. prettyplay/engine/attempts.py +80 -0
  26. prettyplay/engine/classification.py +70 -0
  27. prettyplay/engine/compliance.py +125 -0
  28. prettyplay/engine/execution.py +48 -0
  29. prettyplay/engine/generator.py +1110 -0
  30. prettyplay/engine/groups/.usages/recovery.md +71 -0
  31. prettyplay/engine/groups/__init__.py +7 -0
  32. prettyplay/engine/groups/diagnosis.py +128 -0
  33. prettyplay/engine/groups/outcome.py +55 -0
  34. prettyplay/engine/groups/recovery.py +491 -0
  35. prettyplay/engine/healer.py +149 -0
  36. prettyplay/engine/polling/.usages/settle.md +55 -0
  37. prettyplay/engine/polling/__init__.py +6 -0
  38. prettyplay/engine/polling/settle.py +98 -0
  39. prettyplay/engine/polling/window.py +72 -0
  40. prettyplay/engine/renderer/.usages/memory.md +24 -0
  41. prettyplay/engine/renderer/.usages/rendering.md +44 -0
  42. prettyplay/engine/renderer/.usages/validation.md +20 -0
  43. prettyplay/engine/renderer/__init__.py +7 -0
  44. prettyplay/engine/renderer/memory.py +37 -0
  45. prettyplay/engine/renderer/render.py +184 -0
  46. prettyplay/engine/renderer/validation.py +76 -0
  47. prettyplay/engine/steering/.usages/steering.md +95 -0
  48. prettyplay/engine/steering/__init__.py +5 -0
  49. prettyplay/engine/steering/steering.py +765 -0
  50. prettyplay/engine/text.py +41 -0
  51. prettyplay/executor.py +654 -0
  52. prettyplay/failures/.usages/taxonomy.md +131 -0
  53. prettyplay/failures/__init__.py +25 -0
  54. prettyplay/failures/errors.py +359 -0
  55. prettyplay/groups.py +276 -0
  56. prettyplay/llm/.usages/classification.md +31 -0
  57. prettyplay/llm/.usages/providers.md +127 -0
  58. prettyplay/llm/.usages/transport.md +81 -0
  59. prettyplay/llm/__init__.py +38 -0
  60. prettyplay/llm/_request.py +823 -0
  61. prettyplay/llm/anthropic_provider.py +468 -0
  62. prettyplay/llm/models.py +273 -0
  63. prettyplay/llm/openai_provider.py +444 -0
  64. prettyplay/llm/provider.py +279 -0
  65. prettyplay/reporting/.usages/hooks.md +61 -0
  66. prettyplay/reporting/__init__.py +6 -0
  67. prettyplay/reporting/hooks.py +47 -0
  68. prettyplay/reporting/reporter.py +70 -0
  69. prettyplay/runtime.py +81 -0
  70. prettyplay/scenario.py +469 -0
  71. prettyplay-0.0.2.dist-info/METADATA +515 -0
  72. prettyplay-0.0.2.dist-info/RECORD +75 -0
  73. prettyplay-0.0.2.dist-info/WHEEL +5 -0
  74. prettyplay-0.0.2.dist-info/licenses/LICENSE +28 -0
  75. prettyplay-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,515 @@
1
+ Metadata-Version: 2.4
2
+ Name: prettyplay
3
+ Version: 0.0.2
4
+ Summary: AI-powered UI test generation tool
5
+ License: BSD-3-Clause
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pydantic>=2.7
18
+ Requires-Dist: playwright>=1.49
19
+ Requires-Dist: openai>=1.30
20
+ Requires-Dist: anthropic>=0.28
21
+ Requires-Dist: json-repair>=0.63
22
+ Requires-Dist: jinja2>=3.1
23
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest>=8.0; extra == "test"
26
+ Requires-Dist: pytest-cov>=5.0; extra == "test"
27
+ Requires-Dist: pytest-mock>=3.10; extra == "test"
28
+ Requires-Dist: ruff>=0.15.0; extra == "test"
29
+ Provides-Extra: docs
30
+ Requires-Dist: mkdocs>=1.6.0; extra == "docs"
31
+ Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
32
+ Dynamic: license-file
33
+
34
+ # prettyplay
35
+
36
+ UI tests written as plain sentences. Each step sentence is turned into executable
37
+ code once — by an LLM, against the live page — and cached in the repository.
38
+ Every later run replays the cached code with no LLM involvement at all.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install prettyplay
44
+ playwright install # browser binaries for the driver
45
+ ```
46
+
47
+ Requires Python 3.10+.
48
+
49
+ Full documentation: <https://qarium.github.io/prettyplay/>
50
+
51
+ A complete pytest project — Allure reporting hooks included — lives in the
52
+ `example/` directory of the repository; it additionally requires
53
+ `allure-pytest` (`pip install allure-pytest`), which the core package does
54
+ not depend on.
55
+
56
+ ## Quick start
57
+
58
+ ```python
59
+ from prettyplay import PrettyPlay
60
+
61
+
62
+ def test_login():
63
+ play = PrettyPlay("login-flow")
64
+ play.step("open the login page")
65
+ play.step("enter the login and password")
66
+ play.step("click the Sign in button")
67
+ play.expect("the Welcome message appears")
68
+ play.close()
69
+ ```
70
+
71
+ Or with the context manager:
72
+
73
+ ```python
74
+ with PrettyPlay("login-flow") as play:
75
+ play.step("open the login page")
76
+
77
+ with play.group('Fill login form') as form:
78
+ form.step("enter the login and password")
79
+ form.step("click the Sign in button")
80
+ form.expect("the Welcome message appears")
81
+ ```
82
+
83
+ Each `PrettyPlay` is fully self-contained: it owns its settings, its attempt
84
+ budgets and its own browser session. `close()` (or leaving the `with` block)
85
+ closes the page and stops the whole browser of that test, and every test
86
+ starts with fresh attempt budgets. The old process-wide
87
+ `prettyplay.get_runtime()` singleton was removed — build
88
+ `prettyplay.PrettyplayRuntime(config)` directly if you composed objects over
89
+ it.
90
+
91
+ The constructor arguments form the cache address: `cache_key` (mandatory) and
92
+ `cache_path` (optional subdirectory). Equal keys in the shared root reuse one
93
+ cached step across tests; a different language, step type or key is a
94
+ different step.
95
+
96
+ The third argument overrides settings per test — only the fields you pass
97
+ count: explicitly set values win over pyproject.toml and the environment,
98
+ everything else resolves from the file layer as before. Browser settings form
99
+ the nested `BrowserConfig` group; an empty string inside the group means unset,
100
+ untouched group defaults never overwrite the file values:
101
+
102
+ ```python
103
+ from prettyplay import BrowserConfig, PrettyConfig
104
+
105
+ t = PrettyPlay("login-flow", config=PrettyConfig(browser=BrowserConfig(name="firefox")))
106
+ ```
107
+
108
+ Screenshots belong to the author — nothing is captured automatically. Both
109
+ methods need a step to have run (the page opens lazily):
110
+
111
+ ```python
112
+ png = t.get_screenshot() # full-page PNG bytes
113
+ t.save_screenshot("artifacts/home.png") # write full-page PNG to a file
114
+ ```
115
+
116
+ Stateful page actions excluded from generated code — `page.route`,
117
+ `page.clock`, `add_init_script`, tracing, HAR, CDP — are the author's
118
+ explicit tools through the escape hatch, which runs inside the driver
119
+ worker thread against the genuine page (a step must have run first):
120
+
121
+ ```python
122
+ t.run_on_page(lambda page: page.route("**/api/config", lambda r: r.fulfill(json={"mode": "demo"})))
123
+ title = t.run_on_page(lambda page: page.title())
124
+ ```
125
+
126
+ The action must use the page API only: calling back into the test object
127
+ (`t.step`, screenshots, a nested `t.run_on_page`) marshals into the same
128
+ worker thread the action runs on — the worker rejects the re-entrant
129
+ crossing with a loud error instead of a deadlock.
130
+
131
+ ## What happens on a step
132
+
133
+ Steps can capture page text for later steps. For example, call
134
+ `play.step("Read the item name into {% var name %}")`, then
135
+ `play.expect("The page shows {{ name }}")`. Call-local string values use a
136
+ separate namespace: `play.step("Open {{ vars.code }}", vars={"code": "A1"})`.
137
+ Captures must be nonblank strings read from the page, live for this test only,
138
+ and cannot be declared in `expect`. See
139
+ [Writing steps](docs/guides/writing-steps.md#templates-and-step-memory).
140
+
141
+ - **cache hit** — the cached code runs; no LLM is contacted
142
+ - **cache miss** — the step code is generated (a candidate that must actually
143
+ work on the page), then cached; only successes are cached — after the
144
+ compliance gate (instruction compliance and step adequacy — see
145
+ [Instructions](#instructions)). A candidate
146
+ assertion that legitimately fails stops the retries at once and is
147
+ classified: a real defect fails as `product_defect`; a `rot` or `fixable`
148
+ verdict grants exactly one healing-funded regeneration carrying the
149
+ classification recommendation
150
+ - **cached failure** — the failure is classified:
151
+ - `rot` (the UI changed) — the step is regenerated and the cache rewritten
152
+ - `fixable` (the step code is at fault — an ambiguous or wrong locator —
153
+ while the intent stays satisfiable) — regenerated for the same intent,
154
+ the request carrying the classification recommendation
155
+ - `product_defect` — the test fails loudly; nothing is regenerated
156
+ - `incurable` — the step fails with an explanation and a recommendation
157
+
158
+ A step authored inside a **group block** never takes this per-step path on a
159
+ non-strict run: its failure routes to the group recovery — one diagnosis of
160
+ the whole group drives a group-scoped regeneration row (see
161
+ [Step groups](#step-groups)).
162
+
163
+ Every execution of step code in the step cycle — cached code and
164
+ generation/healing candidates alike — runs under the **settle window** (see
165
+ [Settle polling](#settle-polling)): a transient page-state failure re-executes
166
+ the same code before any costly move. Steering-dialog turns are the one
167
+ exception — see [Interactive steering](#interactive-steering).
168
+
169
+ **Strict replay-only mode** (`strict = true`) never contacts the LLM for code:
170
+ a cache miss fails immediately as `IncurableStepError` ("strict mode forbids
171
+ generation — the step is missing from the cache"), and a failed cached step is
172
+ at most classified — the only LLM call strict mode ever makes — then raised by
173
+ its category (`product_defect` → `ProductDefectError`, everything else — rot
174
+ included — → `IncurableStepError`). Nothing is regenerated or healed, no
175
+ attempt budget is consumed and the cache is never written. The settle window
176
+ still applies to cached code — re-executing it is execution, not generation.
177
+ When the LLM is unavailable, the verdict is skipped with a `WARNING` and the
178
+ step type alone picks the error kind. This is the natural CI posture: generate
179
+ locally, run strict in the pipeline.
180
+
181
+ ## Settle polling
182
+
183
+ `polling_timeout` (default `None` — off; `0` — explicit disable) opens one
184
+ settle window per step execution, measured from the first execution of the
185
+ step's code: a transient failure of a pollable kind — timeouts, element-state
186
+ races, navigation races, failed checks (a failed `expect(...)` chain or a
187
+ plain assert on an immediate read, like `assert videos.count() > 1`) —
188
+ re-executes the same code after `polling_delay` (default 0.5 s) until
189
+ success or window end. Attempts appear as `settle_retry` log records; no LLM
190
+ budget is consumed. Locator ambiguity and Python-level errors of the step
191
+ code (syntax, names, types) never poll. Size the window above the longest
192
+ auto-wait it must absorb (6.0 covers one exhausted 5 s expectation plus one
193
+ re-execution).
194
+
195
+ Every step also takes two keyword-only parameters: `tries` replaces the time
196
+ bound with a count bound for that step (`t.step("open the cart", tries=3)` —
197
+ at most 3 executions in total, the first included; exhaustion propagates the
198
+ failure, the step never stays green on retries alone), and `delay` is a
199
+ quiet pause in seconds before the step's code runs
200
+ (`t.expect("the total updates", delay=1.5)` — the step's `on_step_started`
201
+ event fires, then the pause, then the code). Invalid values fail loudly at
202
+ the call, before any page or LLM involvement.
203
+
204
+ ## Step groups
205
+
206
+ Steps that form one coherent mini-scenario can be authored as a group with a
207
+ shared goal — the group prompt:
208
+
209
+ ```python
210
+ with t.group("the checkout flow", speed=50, delay=2) as g:
211
+ g.step("accept the cookie banner")
212
+ g.step("fill the email field")
213
+ g.expect("the order confirmation appears")
214
+ ```
215
+
216
+ Inside the block the surface is the ordinary one — `g.step`/`g.expect` with
217
+ the `tries`/`delay` parameters — and group membership changes nothing about
218
+ addressing: a group step lands at the same cache address it would outside
219
+ the block, replays LLM-free, and steps outside the block are ordinary steps
220
+ (no ambient rerouting). What changes is failure handling on a non-strict run:
221
+ a failed group step is diagnosed once against the whole interaction — the
222
+ group prompt, every step trace and the failed step's attempt history — and
223
+ the verdict drives a group-scoped recovery row that regenerates the affected
224
+ steps forward on the current page, through the same compliance gate and
225
+ per-step cache write-back as every caching path. A diagnosed product defect
226
+ still fails the test loudly; a root outside the group ends the run honestly;
227
+ recovery cycles per group per test are capped (never an infinite loop). The
228
+ `speed`/`delay` group parameters pace the block — `delay` pauses quietly
229
+ before the first step, `speed` (0–100) inserts
230
+ `int((100 − speed) × 30)` ms between steps as a plain library wait.
231
+
232
+ See the [Groups reference](https://qarium.github.io/prettyplay/reference/groups/)
233
+ for the recovery cycle, the budgets and the log records.
234
+
235
+ ## Interactive steering
236
+
237
+ `interactive = true` (default `false`) arms the steering REPL for local
238
+ generation sessions: when a step terminally fails with
239
+ `IncurableStepError`, a terminal dialog opens — step, failed code, the
240
+ terminal error render, the current page URL, screenshot path, commands —
241
+ and every engineer message drives one regeneration whose complete code is
242
+ shown for approval with `run? [y/N]` before it executes against the live
243
+ page: nothing runs unseen, and a rejected turn enters the history instead
244
+ of the page. Local commands serve the context without an LLM request:
245
+ `snapshot` (the full accessibility snapshot), `screenshot` (a full PNG
246
+ written to a temporary file, path printed), `error` and `code` (the stored
247
+ texts) and `quit`. A green turn heals the step and writes it back to the
248
+ cache — only after the compliance gate passes (a `high` finding in either
249
+ dimension — instruction or adequacy — never reaches the cache: the
250
+ violation joins the history and the prompt reopens); every completed turn
251
+ — executed, red or rejected — appends its full verbatim record (the
252
+ outcome, the URL before → after pair, the complete code, the complete
253
+ outcome) to the shared per-step attempt history of every later request;
254
+ quit, EOF, SIGINT or an unreadable stdin
255
+ raises the original terminal failure. The dialog never opens on
256
+ `product_defect`, in strict mode, or when the provider is down, and
257
+ consumes no budgets. Dialog openings, guidance lines and declines log at
258
+ info level as `steering_opened`, `steering_guidance` and
259
+ `steering_declined`. Keep it off in CI — an accidentally opened dialog
260
+ would hang the run. This is the steering dialog of a stuck step, not an
261
+ interactive host mode (IPython and Jupyter keep working as before).
262
+
263
+ ## Seeing the scenario
264
+
265
+ Step sentences go to the `prettyplay` logger at info level. The library
266
+ configures no handlers — enable logging to see the scenario in the output:
267
+
268
+ ```python
269
+ import logging
270
+
271
+ logging.basicConfig(level=logging.INFO) # plain unittest runs
272
+ ```
273
+
274
+ ```ini
275
+ # pytest: --log-cli-level=INFO
276
+ ```
277
+
278
+ ## Configuration
279
+
280
+ The `[tool.prettyplay]` section of pyproject.toml:
281
+
282
+ ```toml
283
+ [tool.prettyplay]
284
+ provider = "openai" # openai | anthropic
285
+ model = "gpt-5"
286
+ generation_model = "" # optional: empty -> model
287
+ classification_model = "" # optional: empty -> model
288
+ base_url = ""
289
+ cache_root = "" # empty -> <cwd>/.prettyplay/cache/
290
+ generation_prompt = "" # user instructions for generation; empty -> no instructions block
291
+ generation_approve = true # the two-dimension compliance gate (instructions + step adequacy) before caching; false -> the gate never runs
292
+ classification_prompt = "" # user instructions for classification; empty -> no instructions block
293
+ strict = false # true -> replay-only mode (no generation, no healing)
294
+ interactive = false # true -> the steering dialog on a terminally stuck step (local sessions)
295
+ generation_attempts = 3
296
+ healing_attempts = 2
297
+ llm_request_attempts = 3 # total sends per LLM request; 1 disables transport retries
298
+ polling_timeout = 6.0 # settle window seconds; omit -> off, 0.0 -> explicit disable
299
+ polling_delay = 0.5 # pause between settle re-executions
300
+ send_screenshots = false
301
+
302
+ [tool.prettyplay.browser]
303
+ name = "chromium" # chromium | firefox | webkit | chrome | msedge
304
+ screen = "" # "" | WxH | fullscreen | Playwright device name
305
+ headless = true # false -> run with a visible browser window
306
+ endpoint = "" # ws:// endpoint of a remote browser; empty -> local launch
307
+ accept_dialogs = false # true -> accept (else dismiss) dialogs no in-step capture claims
308
+ speed = 100 # pace of the run, 0-100 %; 100 -> full speed (default)
309
+ ```
310
+
311
+ `llm_request_attempts` applies to generation, classification, group diagnosis
312
+ and compliance requests. Its default is 3 total sends, including the first;
313
+ `1` disables retries. Set `PRETTYPLAY_LLM_REQUEST_ATTEMPTS` in the environment
314
+ or pass `PrettyConfig(llm_request_attempts=...)` for one test. Explicit test
315
+ values override the environment, which overrides pyproject.toml. Values must
316
+ be positive integers.
317
+
318
+ The old flat keys `browser`, `headless` and `browser_endpoint` at the
319
+ `[tool.prettyplay]` level are gone — a pre-1.0 hard break: the loader rejects
320
+ them with a `ConfigurationError` naming their new home inside
321
+ `[tool.prettyplay.browser]`, and the removed legacy env name `PRETTYPLAY_BROWSER`
322
+ fails the same way with a hint to use `PRETTYPLAY_BROWSER_NAME`.
323
+
324
+ `chrome` and `msedge` launch the locally installed browser through the
325
+ chromium engine; the browser must be installed on the machine.
326
+
327
+ ### Screen modes
328
+
329
+ The `screen` setting of the browser group decides the context size:
330
+
331
+ - `""` — the Playwright default.
332
+ - `1280x720` (WxH) — a fixed viewport, in every launch mode.
333
+ - `fullscreen` — follows the maximized window on a local headed launch (the
334
+ chromium family launches with `--start-maximized`; firefox/webkit keep their
335
+ plain launch) and pins a fixed 1920x1080 viewport where no window exists —
336
+ headless and remote connects.
337
+ - anything else — a Playwright device name (`iPhone 13`, `Pixel 7`, ...):
338
+ the full device descriptor is applied. An unknown name fails loudly with the
339
+ closest registry names suggested.
340
+
341
+ ### Instructions
342
+
343
+ A non-empty `generation_prompt` is sent verbatim as a `USER INSTRUCTIONS`
344
+ block with every generation and regeneration request — it steers the style of
345
+ the generated code (e.g. `prefer data-test-id attributes`), never the failure
346
+ classification. The instructions are binding, not advisory: while the
347
+ compliance gate is on (`generation_approve`, default `true`),
348
+ every successfully generated candidate passes an independent compliance check
349
+ of two dimensions before it is cached — instruction compliance with these
350
+ instructions, and step adequacy (the code must match what the step sentence
351
+ says for its step type — neither falling short nor exceeding it, judged from
352
+ the verbatim per-step attempt history of what was already tried) — one extra
353
+ LLM call through the effective classification model, never the model that
354
+ wrote the candidate; a `high` finding in either dimension fails the
355
+ attempt and the retry carries the violation with the grown history,
356
+ `medium` and `low` findings pass with a `WARNING`, and a malformed verdict is
357
+ a loud `ComplianceVerdictError` (the candidate is never cached unchecked).
358
+ `generation_approve = false` (env `PRETTYPLAY_GENERATION_APPROVE`) removes the
359
+ gate entirely — the old behavior. The instructions are not part of the cache
360
+ address: changing them never invalidates cached steps — a cached step runs
361
+ unchanged and is never re-gated, so changed instructions need a manual cache
362
+ purge to take effect.
363
+
364
+ A non-empty `classification_prompt` works the same way for classification
365
+ requests only — it steers the verdict explanations (e.g. `answer in Russian`),
366
+ never generation, and never invalidates cached steps either.
367
+
368
+ ### Remote browsers
369
+
370
+ A non-empty `endpoint` (e.g. `ws://ci-grid:3000/playwright/chromium`) connects
371
+ to a remote Playwright Server or browser grid instead of launching locally:
372
+ `headless` does not apply to a connect (window visibility belongs to the
373
+ endpoint server) and `chrome`/`msedge` map to the chromium engine — channels
374
+ are a local-launch concept. A non-empty endpoint must be a valid ws/wss URL
375
+ (otherwise `ConfigurationError` names the setting), and a failed connect fails
376
+ loudly with the endpoint in the message.
377
+
378
+ ### Environment overrides
379
+
380
+ Every setting has a `PRETTYPLAY_<SETTING_UPPER>` environment override for CI —
381
+ including `PRETTYPLAY_STRICT` and `PRETTYPLAY_CLASSIFICATION_PROMPT` — and the
382
+ browser group reads the flat `PRETTYPLAY_BROWSER_NAME`,
383
+ `PRETTYPLAY_BROWSER_SCREEN`, `PRETTYPLAY_BROWSER_HEADLESS`,
384
+ `PRETTYPLAY_BROWSER_ENDPOINT`, `PRETTYPLAY_BROWSER_ACCEPT_DIALOGS` and
385
+ `PRETTYPLAY_BROWSER_SPEED`. Env
386
+ values parse by the field type: booleans
387
+ accept `true/false/1/0` case-insensitively, integers parse as decimals, floats
388
+ (the polling settings) parse as decimal floats, and an
389
+ unparseable value fails loudly with a `ConfigurationError` naming the setting,
390
+ the received value and the accepted form.
391
+
392
+ ### Pace
393
+
394
+ `speed` of the browser group (0–100 inclusive, default 100) controls how fast
395
+ the browser executes the run: 100 — full speed, lower values slow the run
396
+ down linearly through Playwright's native `slow_mo`
397
+ (`int((100 − speed) × 30)` ms between browser operations). One value per
398
+ test, fixed at browser start in every launch mode — local headed, local
399
+ headless and remote connects alike. An out-of-range or malformed value fails
400
+ at configuration load naming `browser.speed`, the received value and the
401
+ accepted form (an integer 0-100 inclusive).
402
+
403
+ An invalid setting fails loudly with a `ConfigurationError`: one line per
404
+ setting — the name, the received value and the allowed values.
405
+
406
+ LLM API keys are never stored in the config file: they come only from the
407
+ environment — `OPENAI_API_KEY` for openai, `ANTHROPIC_API_KEY` for anthropic —
408
+ and are read lazily on the first request.
409
+
410
+ ### Dialogs
411
+
412
+ `accept_dialogs` of the browser group controls how the resolver of last
413
+ resort settles unclaimed dialogs:
414
+
415
+ - `true` — every dialog that no in-step stock dialog capture claims is
416
+ accepted
417
+ - `false` (default) — unclaimed dialogs are dismissed (the Playwright
418
+ default outcome)
419
+ - the resolution runs at the tail of the driver-thread run unit, not at
420
+ the moment the dialog fires: a dialog unclaimed by the step blocks the
421
+ page until the unit ends, which can fail the remainder of the step
422
+ - a dialog claimed by a step's in-step stock capture is accepted or dismissed
423
+ by the step itself — the setting does not apply to captured dialogs
424
+
425
+ ## Failure taxonomy
426
+
427
+ Every library failure derives from `PrettyplayError`:
428
+
429
+ | Exception | Meaning | Recommended reaction |
430
+ |---|---|---|
431
+ | ProductDefectError | real product regression | treat as a bug: this failure is the value of the suite |
432
+ | IncurableStepError | the step cannot be (re)generated — budget exhausted, step text stale, or a strict-mode cache miss | follow `recommendation`: reword the step or refresh the cache |
433
+ | LLMUnavailableError | LLM infrastructure down | restore provider access; cached steps are unaffected |
434
+ | ComplianceVerdictError | the compliance gate could not obtain a usable verdict | rerun the step to retry generation; the candidate was never cached |
435
+ | ConfigurationError | invalid `[tool.prettyplay]` settings | fix the named setting — the message lists received and allowed values |
436
+
437
+ `ProductDefectError` and `IncurableStepError` render one structured terminal
438
+ message — the first line `ClassName: reason`, a `---` separated
439
+ `step:`/`error:` block (the `error:` line carries the decomposed headline
440
+ of the underlying error), a conditional `---` separated details section
441
+ (`received:`, `cause:`, `Call log:` — present only when the underlying
442
+ error carries them), and a `---` separated verdict block with
443
+ `explanation:` and `recommendation:` labels at column zero (the `category`
444
+ travels in the structured fields, never in the render). The
445
+ same single text feeds the exception message, the log record and the
446
+ `on_step_failed` hook payload — consumers never re-compose it. The verdict
447
+ fields also arrive through the `on_step_verdict` hook. `IncurableStepError`
448
+ also carries a `code` attribute — the step code that terminally failed (the
449
+ cached code on healing and strict paths, the last candidate on generation);
450
+ a field for programmatic consumers only, never rendered and never in hook
451
+ payloads.
452
+ `ProductDefectError` also derives from `AssertionError`, so any runner counts
453
+ it as a failed test, never an error. Tracebacks of library failures are folded
454
+ at the `t.step(...)` / `t.expect(...)` call site: internal engine frames
455
+ never appear in what the runner shows.
456
+
457
+ A healed run never turns a `ProductDefectError` into a green test.
458
+
459
+ ## Hooks
460
+
461
+ ```python
462
+ from prettyplay.reporting import StepHooks
463
+
464
+
465
+ class Reporter(StepHooks):
466
+ def on_step_started(self, step_text: str, step_type: str) -> None: ...
467
+ def on_step_passed(self, step_text: str, step_type: str) -> None: ...
468
+ def on_step_failed(self, step_text: str, step_type: str, error: str) -> None: ...
469
+ def on_step_verdict(self, step_text: str, category: str, explanation: str, recommendation: str) -> None: ...
470
+ def on_step_finished(self, step_text: str, step_type: str, outcome: str) -> None: ...
471
+ def on_generation_started(self, step_text: str, attempt: int) -> None: ...
472
+ def on_healing_started(self, step_text: str, category: str) -> None: ...
473
+ def on_healed(self, step_text: str, explanation: str) -> None: ...
474
+ def on_cache_saved(self, step_text: str, filename: str) -> None: ...
475
+ def on_cache_skipped(self, step_text: str, reason: str) -> None: ...
476
+
477
+
478
+ t = PrettyPlay("login-flow")
479
+ t.add_hooks(Reporter())
480
+ ```
481
+
482
+ Hooks can also be wired at construction — pass the list to the keyword-only
483
+ `hooks` parameter (`PrettyPlay("login-flow", hooks=[Reporter()])`) and every
484
+ event of every step reaches them. `StepHooks` is re-exported from the package
485
+ root, so `from prettyplay import StepHooks` works too.
486
+
487
+ For template steps, lifecycle hook `step_text` is the authored template;
488
+ generation and healing hook `step_text` is the rendered instruction.
489
+
490
+ A raising hook never fails the run; the failure is logged. The `error` payload
491
+ of `on_step_failed` is the full structured render of the failure — multi-line,
492
+ verbatim. `on_step_verdict` fires after `on_step_failed`, only when the
493
+ terminal failure carried a real verdict (never for the render-only fallback
494
+ recommendation of a verdict-less `IncurableStepError`).
495
+
496
+ ## Cache and CI workflow
497
+
498
+ The cache lives under `.prettyplay/cache/` as plain Python files — one per
499
+ step, carrying its metadata (step sentence, cache key, step type, creation
500
+ date) and the step code. The code targets the standard Playwright sync API,
501
+ so cached steps keep replaying across library upgrades — the one recorded
502
+ break is the switch to the genuine page: caches written against the retired
503
+ page facade call methods that no longer exist and fail at replay; purge the
504
+ cache directory once and regenerate when upgrading across that change.
505
+
506
+ Generate locally where the LLM is reachable → commit the cache directory →
507
+ CI runs the whole suite from the cache with no LLM keys at all — `strict = true`
508
+ turns that posture into a guarantee: a cache miss fails the run instead of
509
+ silently generating.
510
+
511
+ ## Limitations
512
+
513
+ Step sentences, group prompts, call-local `vars` values and captured values can
514
+ land in LLM prompts, diagnostic output and logs. Do not use them for secrets or
515
+ sensitive personal data.
@@ -0,0 +1,75 @@
1
+ prettyplay/__init__.py,sha256=91ebop-G0VTKlW200V8dwzf3GzFvcfeQFavT1XBWrOs,526
2
+ prettyplay/executor.py,sha256=wOfNb5ARY_m24IijZuS2zukruS5tcF9GcEDWeKth3Ao,28773
3
+ prettyplay/groups.py,sha256=YW6zPuLY6dvijwIaAfDl3iV3GoHKf8LGMzrumIy3bCE,11242
4
+ prettyplay/runtime.py,sha256=vcbE5rNyb-Ntph9OSk0S70sep-L_o3ft3OPFhNXJxLE,2643
5
+ prettyplay/scenario.py,sha256=K_r2RfCWrRUEyuDQdhC6xKp2bzoKlpUNTNmK-QOIM4k,18269
6
+ prettyplay/.usages/lifecycle.md,sha256=5nhN9ihvLQiU0jGeBJUZg9vMGHQemLjCjpobDRHcQII,9263
7
+ prettyplay/.usages/steps.md,sha256=BtzefTSh6L6Kj_0-LaK1dRXYUc-5iAXdY8vVUP-2qkA,9134
8
+ prettyplay/cache/__init__.py,sha256=GYhRRVLAlRCdbKuzo-bMqmaBd4UqQXwch7RUG9zbDO0,318
9
+ prettyplay/cache/budgets.py,sha256=fCF3FXqgJrajqs8v5pAzHiGQStVk8EfEtuJLh3-aCZE,3451
10
+ prettyplay/cache/models.py,sha256=z5FwScZiSitTUDQ-DpOEmD3EhjXsbWTpNYD9X5NX4no,1746
11
+ prettyplay/cache/store.py,sha256=LFfqgSu9CPy42XmS_ZIErCXuNt_v-dgTGdq9kLTCu7c,9944
12
+ prettyplay/cache/text.py,sha256=ijc8iNqtz6IsyHOVmGQZ6Vf40nJJ-SnVyGFozslsBcM,1246
13
+ prettyplay/cache/.usages/addressing.md,sha256=A97hXPUWszYI5RoOeYii6nyMCa9ORwNsoo0hA6wd7r0,2128
14
+ prettyplay/cache/.usages/budgets.md,sha256=shf_By8BJYgAjoN129dBVMAZx5TeDxDO0fpPlsyl3z8,2579
15
+ prettyplay/cache/.usages/storage.md,sha256=uszgU-ExTKK-U2QOiwEL6JQV--Uh2tMM3BnLIhe9T-I,1554
16
+ prettyplay/config/__init__.py,sha256=noOu3FPBlUsQPcSntZswvX3uGNCiEmy54Q9moZh5eHs,273
17
+ prettyplay/config/loader.py,sha256=Yz2ArvPjaQhQRTKfAk_iyCmYRue6QWFULAh03yqxmQE,12041
18
+ prettyplay/config/models.py,sha256=7zh-wsO5foeCRMn-abpFICH5bNtUb5drpM4gSknxJdE,12330
19
+ prettyplay/config/.usages/configuration.md,sha256=FSeTXf0XV1u_yqe3JCvuz7gSXSat7xwAJcGzRUAajHY,13844
20
+ prettyplay/driver/__init__.py,sha256=qAO_BViG0ZxcImMFEkEOe8aYjrGtLfRI8NW3IO0vv50,272
21
+ prettyplay/driver/errors.py,sha256=SPeXGksloHQ1iHlSYQ_LEROhkTnNws10TUkxBDWIIXg,2666
22
+ prettyplay/driver/page.py,sha256=8GBJnLskjqs5NtgU0hmjUHAEgkZQRRPg4-nDKVa4bxk,6875
23
+ prettyplay/driver/session.py,sha256=adEJKwYm7llCTiJLN51exeT9S7WcFxzU74DCs53BvRE,12632
24
+ prettyplay/driver/.usages/error_kinds.md,sha256=becPikNRl7T3CISTFOO0qETOfzF6l01C-zRPD5qP4I4,2158
25
+ prettyplay/driver/.usages/plumbing.md,sha256=aNenMQfkcm7-KmO1E7j55oWEVcsCBSVpVfqbIXTj-hg,3096
26
+ prettyplay/engine/__init__.py,sha256=-VIaet3jxyzZi987NFy5KuvVE78b7r1TACRnXRp_bWY,541
27
+ prettyplay/engine/attempts.py,sha256=I3aoIPiWpNo1ZKSRF5jTUE702_42Aj0SqFRH_iKCSZQ,2771
28
+ prettyplay/engine/classification.py,sha256=-6VH_-76fPI8wRpxtdQqpmZ3gXPfec6Jm1KJcnygoBE,3024
29
+ prettyplay/engine/compliance.py,sha256=Q5FzfYfmQSTBMk2vIjCdrRvwJbFXn0dFHHa2TjOiNyI,7032
30
+ prettyplay/engine/execution.py,sha256=2mrWsv9ULZxMYbYBFQSyDTXAdJoLcg9XuuIPUehz46k,1977
31
+ prettyplay/engine/generator.py,sha256=VAYYLlu24DM9L_Oi0kLPSgBbdAkMV_EAATb-_gOHg7U,53986
32
+ prettyplay/engine/healer.py,sha256=DeGUh6F1NlnZNDkF1agN0y660-Pl7feysExkIOQo1EA,7124
33
+ prettyplay/engine/text.py,sha256=SQuVzz9gTD4A88jycXu5HzVuZ9-SWMfIXlMFT4RdxjQ,1516
34
+ prettyplay/engine/.usages/generation.md,sha256=6ymO9Fog7MKaX_qh8eeb4KUu144ccTdJvWf81Ko3zvU,10890
35
+ prettyplay/engine/.usages/healing.md,sha256=SxuI7hE8yL4dyuD0W8mJuBIB40ctz8xm3TnM5ikk3CE,4367
36
+ prettyplay/engine/groups/__init__.py,sha256=2D2F2MZYXs92b8ZvfhB6jG8-KKj-zUrvMm7bLr4b7pQ,293
37
+ prettyplay/engine/groups/diagnosis.py,sha256=9PT5PGj32lkJhjxpGYow9SJ-ohI8kPfs4MLw0sin0MU,5760
38
+ prettyplay/engine/groups/outcome.py,sha256=gcWGYa9zVNH1bqCn5pB2TIPnygmtP92G_UVDMopPKAU,2336
39
+ prettyplay/engine/groups/recovery.py,sha256=PlGuVG7UAmG8T8nWA9R4HoqVbBi_wWSBXMIYeMMSA9Y,22241
40
+ prettyplay/engine/groups/.usages/recovery.md,sha256=v7Oi2e1tDGCUgeHBuNsBcUcKDzWxERIkDjiwsJpbWto,4020
41
+ prettyplay/engine/polling/__init__.py,sha256=aEbl8NFKBLgPnvoSZZKVgmc5gUogcnfuWuwmmt49mhk,196
42
+ prettyplay/engine/polling/settle.py,sha256=oqaLXegMWK5qMQ2ZGx0PbBiiLkwPsAuKaok20WWwzoA,3303
43
+ prettyplay/engine/polling/window.py,sha256=_kq3T_JlFDIda2ZHgwVeAMkNfZ7xWGY0CNs7mk_6tPI,2932
44
+ prettyplay/engine/polling/.usages/settle.md,sha256=VqWCCEw3B0pe4S8uLb4hV00vhjr9GRNj9T6I78cjgsU,3162
45
+ prettyplay/engine/renderer/__init__.py,sha256=q4OTSJX83TUm7HIQ8BQzqnfnmaSmGZI9PjQuSIHEIGk,329
46
+ prettyplay/engine/renderer/memory.py,sha256=bpMgEfh-I-XP3FpoJ-fJL4sW2IK8p69fqrtBB1Qvi58,1277
47
+ prettyplay/engine/renderer/render.py,sha256=Qi0TitjP_ZCJIzyYgOsL3qlq88O50pLGV2l_oTIIHlQ,6970
48
+ prettyplay/engine/renderer/validation.py,sha256=WhpfNvh0x4JB86q6PFpzHtbRhCM996obwaOVMUPuad4,2843
49
+ prettyplay/engine/renderer/.usages/memory.md,sha256=xuKr33yJIvhwGxnPWAsraTu9_jIh6W4Is7gJqt4kKE4,1203
50
+ prettyplay/engine/renderer/.usages/rendering.md,sha256=OsH6KJwLuwoQPT7PuNUWlqs9rNKT1qdX9rrrQOin77o,1724
51
+ prettyplay/engine/renderer/.usages/validation.md,sha256=Nld-ksul_XNc1aPW0y0mp10WjsgNMS2rSDOqA7ooy8k,1172
52
+ prettyplay/engine/steering/__init__.py,sha256=Q_aStUOjoGj6CuDXyWQTy2efQFZtnxxqqeQYPZCxPaY,150
53
+ prettyplay/engine/steering/steering.py,sha256=N02eXZHmr9B20N-PLENTncb6AxarItmZEjRlGYE2vdA,36115
54
+ prettyplay/engine/steering/.usages/steering.md,sha256=Tb9uHsvi9ZQkDLlyRVBfydNWnfsKrq3nuPQTT4qsfmA,5537
55
+ prettyplay/failures/__init__.py,sha256=5lyYu2fkqsIivYQt23il6Aeb09NfbZQ9kpshgJd2dwI,567
56
+ prettyplay/failures/errors.py,sha256=43LnujdpT0ntCUPX98qlwckz9Pdb2cUaNypoBIAP4gk,12564
57
+ prettyplay/failures/.usages/taxonomy.md,sha256=QLVMNXPeVhAGg8sjPXd1UHxSZRFJmmjUpTrt-KZ1B-E,7285
58
+ prettyplay/llm/__init__.py,sha256=WRE3T0I6UjqJncbMiIGMmfScHlOnK8Zjv1BRHDJD6SI,1039
59
+ prettyplay/llm/_request.py,sha256=l39sulkxzF-ulcE_vKM5XECWH9KcT-HW5gzKRIFDq3g,33313
60
+ prettyplay/llm/anthropic_provider.py,sha256=98rs0swMbVbAq4oRkS0OPc2QZMVV_8yfRvUQZuMSsKQ,21014
61
+ prettyplay/llm/models.py,sha256=cJHwIO2y7OcUVxTnCuewfmNqKpB7vZrmMw9LCKzxDOE,9818
62
+ prettyplay/llm/openai_provider.py,sha256=5dE4I418o3bjwywfEU9GyetomyLw7vnCqrw_HC7Kr-k,19927
63
+ prettyplay/llm/provider.py,sha256=d6Pql528p12zD5TGpE1e7pFbDrPjyKKe-2MdRLZKpJM,14602
64
+ prettyplay/llm/.usages/classification.md,sha256=sE7qSQcRzW8tV5RP7REWet3JQT2cTPdKl8W7_5w_ZIk,1801
65
+ prettyplay/llm/.usages/providers.md,sha256=FhmnaeYh3d7vW15_A1COtGaSETFkDPpHfimPIgseuPE,10571
66
+ prettyplay/llm/.usages/transport.md,sha256=N0hqRlZ6pOUJV_agGAr2aiH_lEBtnZfYcJF1Vug1nEQ,3258
67
+ prettyplay/reporting/__init__.py,sha256=_dmLcKn7WDDqBWNAHT0VTRHPZu2-nrMpfux60gASTU0,187
68
+ prettyplay/reporting/hooks.py,sha256=u7Ns6SIJ03R5rHJLDXun_pnHL1Q0oe94-V4ma98g1gI,2535
69
+ prettyplay/reporting/reporter.py,sha256=TlOdgCIrmUZCobxAhUzmq1qs2ycjiNMR9HOaiwfxFXM,2325
70
+ prettyplay/reporting/.usages/hooks.md,sha256=Vipy_4_HbPGMdwv0Kq6UQMQJfN_idQO406Ck4388LXI,4857
71
+ prettyplay-0.0.2.dist-info/licenses/LICENSE,sha256=3fteeL9wFLa2P6MykXx6RkApbO0N9NUrSs9rVH8gxnc,1493
72
+ prettyplay-0.0.2.dist-info/METADATA,sha256=ILjZ8LvRBihP0js_TEx-8WXW_4cnv0YumOKlaiJR2Pk,25440
73
+ prettyplay-0.0.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
74
+ prettyplay-0.0.2.dist-info/top_level.txt,sha256=ZByeTYDEYyWHvSGaI4cQYCrh1oiDpK5bTfnwv_ogWUg,11
75
+ prettyplay-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, qarium
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ prettyplay