prettyplay 0.0.0__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.

Potentially problematic release.


This version of prettyplay might be problematic. Click here for more details.

Files changed (58) hide show
  1. prettyplay/.usages/lifecycle.md +50 -0
  2. prettyplay/.usages/steps.md +56 -0
  3. prettyplay/CODEMANIFEST +182 -0
  4. prettyplay/__init__.py +13 -0
  5. prettyplay/cache/.usages/addressing.md +31 -0
  6. prettyplay/cache/.usages/budgets.md +21 -0
  7. prettyplay/cache/.usages/storage.md +32 -0
  8. prettyplay/cache/CODEMANIFEST +152 -0
  9. prettyplay/cache/__init__.py +8 -0
  10. prettyplay/cache/budgets.py +73 -0
  11. prettyplay/cache/models.py +60 -0
  12. prettyplay/cache/store.py +260 -0
  13. prettyplay/cache/text.py +35 -0
  14. prettyplay/config/.usages/configuration.md +87 -0
  15. prettyplay/config/CODEMANIFEST +132 -0
  16. prettyplay/config/__init__.py +6 -0
  17. prettyplay/config/loader.py +192 -0
  18. prettyplay/config/models.py +92 -0
  19. prettyplay/driver/.usages/facade.md +66 -0
  20. prettyplay/driver/CODEMANIFEST +162 -0
  21. prettyplay/driver/__init__.py +6 -0
  22. prettyplay/driver/page.py +350 -0
  23. prettyplay/driver/session.py +288 -0
  24. prettyplay/engine/.usages/generation.md +45 -0
  25. prettyplay/engine/.usages/healing.md +31 -0
  26. prettyplay/engine/CODEMANIFEST +215 -0
  27. prettyplay/engine/__init__.py +8 -0
  28. prettyplay/engine/classification.py +69 -0
  29. prettyplay/engine/execution.py +25 -0
  30. prettyplay/engine/generator.py +318 -0
  31. prettyplay/engine/healer.py +116 -0
  32. prettyplay/engine/text.py +19 -0
  33. prettyplay/executor.py +109 -0
  34. prettyplay/failures/.usages/taxonomy.md +40 -0
  35. prettyplay/failures/CODEMANIFEST +117 -0
  36. prettyplay/failures/__init__.py +17 -0
  37. prettyplay/failures/errors.py +147 -0
  38. prettyplay/llm/.usages/classification.md +25 -0
  39. prettyplay/llm/.usages/providers.md +33 -0
  40. prettyplay/llm/CODEMANIFEST +125 -0
  41. prettyplay/llm/__init__.py +8 -0
  42. prettyplay/llm/_request.py +216 -0
  43. prettyplay/llm/anthropic_provider.py +213 -0
  44. prettyplay/llm/models.py +22 -0
  45. prettyplay/llm/openai_provider.py +187 -0
  46. prettyplay/llm/provider.py +115 -0
  47. prettyplay/reporting/.usages/hooks.md +41 -0
  48. prettyplay/reporting/CODEMANIFEST +79 -0
  49. prettyplay/reporting/__init__.py +6 -0
  50. prettyplay/reporting/hooks.py +37 -0
  51. prettyplay/reporting/reporter.py +70 -0
  52. prettyplay/runtime.py +107 -0
  53. prettyplay/scenario.py +291 -0
  54. prettyplay-0.0.0.dist-info/METADATA +236 -0
  55. prettyplay-0.0.0.dist-info/RECORD +58 -0
  56. prettyplay-0.0.0.dist-info/WHEEL +5 -0
  57. prettyplay-0.0.0.dist-info/licenses/LICENSE +28 -0
  58. prettyplay-0.0.0.dist-info/top_level.txt +1 -0
prettyplay/scenario.py ADDED
@@ -0,0 +1,291 @@
1
+ """The integrator-facing object: one PrettyTest per test owns the addressing and the page."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import types
6
+ from pathlib import Path
7
+ from types import TracebackType
8
+
9
+ from .cache import StepCache
10
+ from .config import PrettyConfig, load_config
11
+ from .driver import PageFacade
12
+ from .engine import StepGenerator, StepHealer
13
+ from .executor import StepExecutor
14
+ from .failures.errors import PrettyplayError
15
+ from .reporting import StepHooks, StepReporter
16
+ from .runtime import PrettyplayRuntime
17
+
18
+
19
+ def _raise_folded(error: PrettyplayError) -> types.NoReturn:
20
+ """Re-raise a library failure with its traceback folded to this boundary.
21
+
22
+ The head link of the caught traceback is the facade method's own frame;
23
+ the folded traceback keeps exactly that link, so the internal library
24
+ frames (engine, healing, provider) never appear in what the runner shows.
25
+ The same exception object is re-raised — never a copy — keeping identity
26
+ for hook consumers and ``except`` clauses, and adding no context nesting.
27
+ The chained exceptions (``__context__``/``__cause__``) keep their identity
28
+ and messages — the original step failure stays visible for debugging —
29
+ but their tracebacks are folded away too: a runner that renders the chain
30
+ shows no internal frames either.
31
+
32
+ Args:
33
+ error: the library failure leaving ``action``/``assertion``.
34
+
35
+ Raises:
36
+ Always: the given error with the folded traceback attached.
37
+ """
38
+ tb = error.__traceback__
39
+ _fold_chain_tracebacks(error)
40
+
41
+ folded = types.TracebackType(
42
+ tb_next=None,
43
+ tb_frame=tb.tb_frame,
44
+ tb_lasti=tb.tb_lasti,
45
+ tb_lineno=tb.tb_lineno,
46
+ )
47
+
48
+ raise error.with_traceback(folded)
49
+
50
+
51
+ def _fold_chain_tracebacks(error: BaseException) -> None:
52
+ """Drop the tracebacks of the chained exceptions, keeping the chains themselves.
53
+
54
+ Args:
55
+ error: the exception whose ``__context__``/``__cause__`` chains are folded.
56
+ """
57
+ pending = [error]
58
+ seen: set[int] = set()
59
+
60
+ while pending:
61
+ current = pending.pop()
62
+
63
+ if id(current) in seen:
64
+ continue
65
+
66
+ seen.add(id(current))
67
+
68
+ for link in (current.__context__, current.__cause__):
69
+ if link is not None:
70
+ link.__traceback__ = None
71
+ pending.append(link)
72
+
73
+
74
+ class PrettyTest:
75
+ """The main object of the integrator: one instance per UI test.
76
+
77
+ The test writes its scenario in plain sentences — :meth:`action` and
78
+ :meth:`assertion` — and this object does the rest: addressing of the
79
+ cached steps by the context key, the isolated page of the test, and the
80
+ full step cycle delegated to the executor. Construction is cheap: the
81
+ page opens lazily on the first step and no LLM credential is needed.
82
+ Every test composes its own runtime here — no process-wide state, no
83
+ state leaks between tests; two tests in one process hold two runtimes,
84
+ two attempt registries and two browser sessions.
85
+
86
+ Attributes:
87
+ _runtime: the composition root owned by this one test.
88
+ _reporter: the visibility point of this test.
89
+ _cache: the step cache of this test.
90
+ _generator: the generation engine of this test.
91
+ _healer: the healing engine of this test.
92
+ _executor: the owner of the step cycle of this test.
93
+ _page: the isolated page of this test; ``None`` until the first step.
94
+ """
95
+
96
+ def __init__(self, cache_key: str, cache_path: str | None = None, config: PrettyConfig | None = None) -> None:
97
+ """Compose the per-test objects over the runtime this test owns.
98
+
99
+ Args:
100
+ cache_key: the context key of the test; the addressing part of
101
+ every cached step of this scenario.
102
+ cache_path: the optional subdirectory inside the cache; part of
103
+ the address, so steps of different subdirectories never
104
+ collide.
105
+ config: the programmatic settings layer; explicitly set values
106
+ win over the pyproject+env file layer, unset and empty
107
+ fields resolve from it. ``None`` resolves everything from
108
+ the file layer, as before.
109
+ """
110
+ effective = load_config(None, config)
111
+ self._runtime = PrettyplayRuntime(effective)
112
+ self._reporter = StepReporter(hooks=[])
113
+ self._cache = StepCache(self._runtime.config, cache_path, self._reporter)
114
+
115
+ self._generator = StepGenerator(
116
+ self._runtime.config,
117
+ self._runtime.provider,
118
+ self._cache,
119
+ self._runtime.budgets,
120
+ self._reporter,
121
+ )
122
+ self._healer = StepHealer(
123
+ self._runtime.config,
124
+ self._runtime.provider,
125
+ self._generator,
126
+ self._cache,
127
+ self._runtime.budgets,
128
+ self._reporter,
129
+ )
130
+ self._executor = StepExecutor(
131
+ cache_key,
132
+ self._cache,
133
+ self._generator,
134
+ self._healer,
135
+ self._runtime.budgets,
136
+ self._reporter,
137
+ )
138
+ self._page: PageFacade | None = None
139
+
140
+ @property
141
+ def cache_key(self) -> str:
142
+ """The context key of this test, for diagnostics."""
143
+ return self._executor.cache_key
144
+
145
+ def _ensure_page(self) -> PageFacade:
146
+ """Open the isolated page of this test lazily, exactly once.
147
+
148
+ Returns:
149
+ The page facade of this test.
150
+ """
151
+ if self._page is None:
152
+ self._page = self._runtime.open_page()
153
+ return self._page
154
+
155
+ def action(self, text: str) -> None:
156
+ """Execute one action step sentence through the step cycle.
157
+
158
+ A library failure leaving this method carries its traceback folded to
159
+ this boundary: the runner sees the test frame and the boundary frame
160
+ with the rendered message, never the internal engine frames.
161
+
162
+ Args:
163
+ text: the sentence of the action as written by the engineer.
164
+
165
+ Raises:
166
+ ProductDefectError: the step expectation is genuinely broken in the product.
167
+ IncurableStepError: the step never generated successfully, or the verdict
168
+ says regeneration cannot help.
169
+ LlmUnavailableError: the provider service failed; no retry.
170
+ """
171
+ try:
172
+ self._executor.execute(text, "action", self._ensure_page())
173
+ except PrettyplayError as error:
174
+ _raise_folded(error)
175
+
176
+ def assertion(self, text: str) -> None:
177
+ """Execute one assertion step sentence through the step cycle.
178
+
179
+ A library failure leaving this method carries its traceback folded to
180
+ this boundary, exactly as :meth:`action` does.
181
+
182
+ Args:
183
+ text: the sentence of the assertion as written by the engineer.
184
+
185
+ Raises:
186
+ ProductDefectError: the step expectation is genuinely broken in the product.
187
+ IncurableStepError: the step never generated successfully, or the verdict
188
+ says regeneration cannot help.
189
+ LlmUnavailableError: the provider service failed; no retry.
190
+ """
191
+ try:
192
+ self._executor.execute(text, "assertion", self._ensure_page())
193
+ except PrettyplayError as error:
194
+ _raise_folded(error)
195
+
196
+ def get_screenshot(self) -> bytes:
197
+ """Return a full-page PNG screenshot of the current test page.
198
+
199
+ The decision to take a screenshot belongs to the author: nothing is
200
+ captured automatically on step failures.
201
+
202
+ Returns:
203
+ The PNG image bytes of the page.
204
+
205
+ Raises:
206
+ PrettyplayError: no test page exists yet — run a step first.
207
+ """
208
+ if self._page is None:
209
+ raise PrettyplayError("no test page yet: run a step first — the page opens lazily on the first step")
210
+
211
+ return self._page.screenshot()
212
+
213
+ def save_screenshot(self, filepath: str) -> None:
214
+ """Save a full-page PNG screenshot of the current test page to a file.
215
+
216
+ Parent directories are not created: nothing is created silently —
217
+ a missing directory is a loud failure.
218
+
219
+ Args:
220
+ filepath: the destination path of the PNG file.
221
+
222
+ Raises:
223
+ PrettyplayError: no test page exists yet, or the file cannot be
224
+ written; the original ``OSError`` is chained.
225
+ """
226
+ if self._page is None:
227
+ raise PrettyplayError("no test page yet: run a step first — the page opens lazily on the first step")
228
+
229
+ image = self._page.screenshot()
230
+
231
+ try:
232
+ Path(filepath).write_bytes(image)
233
+ except OSError as error:
234
+ raise PrettyplayError(f"cannot write the screenshot to {filepath}: {error}") from error
235
+
236
+ def add_hooks(self, hooks: StepHooks) -> None:
237
+ """Register integrator hooks for the events of this test.
238
+
239
+ Args:
240
+ hooks: the hooks object; register before the first step to see
241
+ every event of the scenario.
242
+ """
243
+ self._reporter.hooks.append(hooks)
244
+
245
+ def close(self) -> None:
246
+ """Close the page and the whole runtime of this test.
247
+
248
+ The isolated page context closes first, then the runtime stops
249
+ unconditionally — a failing page close (e.g. after a browser crash)
250
+ never keeps the browser of the test alive; its error still propagates.
251
+ The browser of this test does not outlive the test.
252
+
253
+ Idempotent and safe before the first step: nothing was opened —
254
+ nothing is closed beyond the no-op runtime close. The page reference
255
+ drops before its close runs, so even a failing page close never
256
+ repeats on a retried ``close``.
257
+ """
258
+ page = self._page
259
+ self._page = None
260
+
261
+ try:
262
+ if page is not None:
263
+ page.close()
264
+ finally:
265
+ self._runtime.close()
266
+
267
+ def __enter__(self) -> PrettyTest:
268
+ """Enter the scenario block of one test.
269
+
270
+ Returns:
271
+ This test object.
272
+ """
273
+ return self
274
+
275
+ def __exit__(
276
+ self,
277
+ exc_type: type[BaseException] | None,
278
+ exc_value: BaseException | None,
279
+ traceback: TracebackType | None,
280
+ ) -> None:
281
+ """Leave the scenario block: close the test and never suppress.
282
+
283
+ Args:
284
+ exc_type: the type of the block exception, if any.
285
+ exc_value: the block exception, if any.
286
+ traceback: the traceback of the block exception, if any.
287
+
288
+ Returns:
289
+ Nothing — a falsy return keeps the exception propagating.
290
+ """
291
+ self.close()
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.4
2
+ Name: prettyplay
3
+ Version: 0.0.0
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: tomli>=2.0; python_version < "3.11"
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest>=8.0; extra == "test"
24
+ Requires-Dist: pytest-cov>=5.0; extra == "test"
25
+ Requires-Dist: pytest-mock>=3.10; extra == "test"
26
+ Requires-Dist: ruff>=0.15.0; extra == "test"
27
+ Provides-Extra: docs
28
+ Requires-Dist: mkdocs>=1.6.0; extra == "docs"
29
+ Requires-Dist: mkdocs-material>=9.5.0; extra == "docs"
30
+ Dynamic: license-file
31
+
32
+ # prettyplay
33
+
34
+ UI tests written as plain sentences. Each step sentence is turned into executable
35
+ code once — by an LLM, against the live page — and cached in the repository.
36
+ Every later run replays the cached code with no LLM involvement at all.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install prettyplay
42
+ playwright install # browser binaries for the driver
43
+ ```
44
+
45
+ Requires Python 3.10+.
46
+
47
+ ## Quick start
48
+
49
+ ```python
50
+ from prettyplay import PrettyTest
51
+
52
+
53
+ def test_login():
54
+ t = PrettyTest("login-flow")
55
+ t.action("открыть страницу логина")
56
+ t.action("ввести логин и пароль")
57
+ t.action("нажать «Войти»")
58
+ t.assertion("появилась надпись «Добро пожаловать»")
59
+ t.close()
60
+ ```
61
+
62
+ Or with the context manager:
63
+
64
+ ```python
65
+ with PrettyTest("login-flow") as t:
66
+ t.action("открыть страницу логина")
67
+ ```
68
+
69
+ Each `PrettyTest` is fully self-contained: it owns its settings, its attempt
70
+ budgets and its own browser session. `close()` (or leaving the `with` block)
71
+ closes the page and stops the whole browser of that test, and every test
72
+ starts with fresh attempt budgets. The old process-wide
73
+ `prettyplay.get_runtime()` singleton was removed — build
74
+ `prettyplay.PrettyplayRuntime(config)` directly if you composed objects over
75
+ it.
76
+
77
+ The constructor arguments form the cache address: `cache_key` (mandatory) and
78
+ `cache_path` (optional subdirectory). Equal keys in the shared root reuse one
79
+ cached step across tests; a different language, step type or key is a
80
+ different step.
81
+
82
+ The third argument overrides settings per test — only the fields you pass
83
+ count: explicitly set values win over pyproject.toml and the environment,
84
+ everything else resolves from the file layer as before:
85
+
86
+ ```python
87
+ from prettyplay import PrettyConfig
88
+
89
+ t = PrettyTest("login-flow", config=PrettyConfig(browser="firefox"))
90
+ ```
91
+
92
+ Screenshots belong to the author — nothing is captured automatically. Both
93
+ methods need a step to have run (the page opens lazily):
94
+
95
+ ```python
96
+ png = t.get_screenshot() # full-page PNG bytes
97
+ t.save_screenshot("artifacts/home.png") # write full-page PNG to a file
98
+ ```
99
+
100
+ ## What happens on a step
101
+
102
+ - **cache hit** — the cached code runs; no LLM is contacted
103
+ - **cache miss** — the step code is generated (a candidate that must actually
104
+ work on the page), then cached; only successes are cached. A candidate
105
+ assertion that legitimately fails stops the retries at once and is
106
+ classified: a real defect fails as `product_defect` instead of burning the
107
+ attempt budget
108
+ - **cached failure** — the failure is classified:
109
+ - `rot` (the UI changed) — the step is regenerated and the cache rewritten
110
+ - `product_defect` — the test fails loudly; nothing is regenerated
111
+ - `incurable` — the step fails with an explanation and a recommendation
112
+
113
+ ## Seeing the scenario
114
+
115
+ Step sentences go to the `prettyplay` logger at info level. The library
116
+ configures no handlers — enable logging to see the scenario in the output:
117
+
118
+ ```python
119
+ import logging
120
+
121
+ logging.basicConfig(level=logging.INFO) # plain unittest runs
122
+ ```
123
+
124
+ ```ini
125
+ # pytest: --log-cli-level=INFO
126
+ ```
127
+
128
+ ## Configuration
129
+
130
+ The `[tool.prettyplay]` section of pyproject.toml:
131
+
132
+ ```toml
133
+ [tool.prettyplay]
134
+ provider = "openai" # openai | anthropic
135
+ browser = "chromium" # chromium | firefox | webkit | chrome | msedge
136
+ model = "gpt-5"
137
+ generation_model = "" # optional: empty -> model
138
+ classification_model = "" # optional: empty -> model
139
+ base_url = ""
140
+ cache_root = "" # empty -> <repo>/.prettyplay/cache/
141
+ generation_prompt = "" # user instructions for generation; empty -> no instructions block
142
+ browser_endpoint = "" # ws:// endpoint of a remote browser; empty -> local launch
143
+ generation_attempts = 3
144
+ healing_attempts = 2
145
+ send_screenshots = false
146
+ headless = true # false -> run with a visible browser window
147
+ ```
148
+
149
+ `chrome` and `msedge` launch the locally installed browser through the
150
+ chromium engine; the browser must be installed on the machine.
151
+
152
+ A non-empty `generation_prompt` is sent verbatim as a `USER INSTRUCTIONS`
153
+ block with every generation and regeneration request — it steers the style of
154
+ the generated code (e.g. `prefer data-test-id attributes`), never the failure
155
+ classification. The instructions are not part of the cache address: changing
156
+ them never invalidates cached steps — a cached step runs unchanged.
157
+
158
+ A non-empty `browser_endpoint` (e.g. `ws://ci-grid:3000/playwright/chromium`)
159
+ connects to a remote Playwright Server or browser grid instead of launching
160
+ locally: `headless` does not apply to a connect (window visibility belongs to
161
+ the endpoint server) and `chrome`/`msedge` map to the chromium engine —
162
+ channels are a local-launch concept. A non-empty endpoint must be a valid
163
+ ws/wss URL (otherwise `ConfigurationError` names the setting), and a failed
164
+ connect fails loudly with the endpoint in the message.
165
+
166
+ Every setting has a `PRETTYPLAY_<SETTING_UPPER>` environment override for CI,
167
+ except the browser (`PRETTYPLAY_BROWSER_NAME`) and headless
168
+ (`PRETTYPLAY_BROWSER_HEADLESS`). The removed legacy name `PRETTYPLAY_BROWSER`
169
+ fails immediately with a hint to use `PRETTYPLAY_BROWSER_NAME`.
170
+
171
+ An invalid setting fails loudly with a `ConfigurationError`: one line per
172
+ setting — the name, the received value and the allowed values.
173
+
174
+ LLM API keys are never stored in the config file: they come only from the
175
+ environment — `OPENAI_API_KEY` for openai, `ANTHROPIC_API_KEY` for anthropic —
176
+ and are read lazily on the first request.
177
+
178
+ ## Failure taxonomy
179
+
180
+ Every library failure derives from `PrettyplayError`:
181
+
182
+ | Exception | Meaning | Recommended reaction |
183
+ |---|---|---|
184
+ | ProductDefectError | real product regression | treat as a bug: this failure is the value of the suite |
185
+ | IncurableStepError | the step cannot be (re)generated | follow `recommendation`: reword the step or refresh the cache |
186
+ | LlmUnavailableError | LLM infrastructure down | restore provider access; cached steps are unaffected |
187
+ | ConfigurationError | invalid `[tool.prettyplay]` settings | fix the named setting — the message lists received and allowed values |
188
+
189
+ `ProductDefectError` and `IncurableStepError` carry a verdict — `category`,
190
+ `explanation`, `recommendation` from the failure classification — appended to
191
+ the exception message and delivered through the `on_step_verdict` hook.
192
+ `ProductDefectError` also derives from `AssertionError`, so any runner counts
193
+ it as a failed test, never an error. Tracebacks of library failures are folded
194
+ at the `t.action(...)` / `t.assertion(...)` call site: internal engine frames
195
+ never appear in what the runner shows.
196
+
197
+ A healed run never turns a `ProductDefectError` into a green test.
198
+
199
+ ## Hooks
200
+
201
+ ```python
202
+ from prettyplay.reporting import StepHooks
203
+
204
+
205
+ class Reporter(StepHooks):
206
+ def on_step_started(self, step_text: str, step_type: str) -> None: ...
207
+ def on_step_passed(self, step_text: str, step_type: str) -> None: ...
208
+ def on_step_failed(self, step_text: str, step_type: str, error: str) -> None: ...
209
+ def on_step_verdict(self, step_text: str, category: str, explanation: str, recommendation: str) -> None: ...
210
+ def on_generation_started(self, step_text: str, attempt: int) -> None: ...
211
+ def on_healing_started(self, step_text: str, category: str) -> None: ...
212
+ def on_healed(self, step_text: str, explanation: str) -> None: ...
213
+ def on_cache_saved(self, step_text: str, filename: str) -> None: ...
214
+ def on_cache_skipped(self, step_text: str, reason: str) -> None: ...
215
+
216
+
217
+ t = PrettyTest("login-flow")
218
+ t.add_hooks(Reporter())
219
+ ```
220
+
221
+ A raising hook never fails the run; the failure is logged. `on_step_verdict`
222
+ fires after `on_step_failed`, only when the terminal failure carried a verdict.
223
+
224
+ ## Cache and CI workflow
225
+
226
+ The cache lives under `.prettyplay/cache/` as plain Python files — one per
227
+ step, carrying its metadata (step sentence, cache key, step type, creation
228
+ date) and the step code.
229
+
230
+ Generate locally where the LLM is reachable → commit the cache directory →
231
+ CI runs the whole suite from the cache with no LLM keys at all.
232
+
233
+ ## Limitations
234
+
235
+ Step sentences land in the repository cache, the logs and the LLM requests:
236
+ never put secrets or personal data into a step.
@@ -0,0 +1,58 @@
1
+ prettyplay/CODEMANIFEST,sha256=J8KN00rNtkG7hA70qLSvxhBoCRiMVjH1l_kouZu5X1g,9781
2
+ prettyplay/__init__.py,sha256=pjAKVu2otS-4MqnTlIyXjoM54_L4TkwwMVd8bkhrRYs,393
3
+ prettyplay/executor.py,sha256=AJ5JApn24yop2e9mFEgC03qqZ2amAnYH-Hh4KQMakpU,4825
4
+ prettyplay/runtime.py,sha256=-HvXhykBa8MoLa7Tq-TWFG0I8X8u9Ql9SX6T2iC6qyQ,4203
5
+ prettyplay/scenario.py,sha256=dtmP4ol_QeHE5ia2LV9NG3AzfLLlfwLBbXlnLQPtlis,10691
6
+ prettyplay/.usages/lifecycle.md,sha256=XRHXkSef2tQU-901F6bPQFDYN981zBBic4B6uiP6-KY,3561
7
+ prettyplay/.usages/steps.md,sha256=okTZ8Gmf7AY2hOcw4TqL_c8Oq3mphNY8myuuaU2mWM4,1903
8
+ prettyplay/cache/CODEMANIFEST,sha256=fvCVBwA1hmYWbjps3x_4rh6agPMo_bmqkFCLtrzJhUk,6897
9
+ prettyplay/cache/__init__.py,sha256=GYhRRVLAlRCdbKuzo-bMqmaBd4UqQXwch7RUG9zbDO0,318
10
+ prettyplay/cache/budgets.py,sha256=Q2BDNknt309G-2oXY2IL0NxxpgFoIDDuEJC_yr7t8Ac,2819
11
+ prettyplay/cache/models.py,sha256=Qkj8re-CUVD68NUdTZdc1UyvcagDRFCLobs4GL_Erjs,1987
12
+ prettyplay/cache/store.py,sha256=tjwyTioi1zu-SNzBMMWPtThuJUWsh__BASvm15x5y5g,9168
13
+ prettyplay/cache/text.py,sha256=a1dd-ZrMIdykihxNcInC8a3zGkSis5dfxechxcfvvSo,1199
14
+ prettyplay/cache/.usages/addressing.md,sha256=9h2Fv-KIPQamvKmHE1PGLX0vKuqs6IQqJiKWCPxMibY,1327
15
+ prettyplay/cache/.usages/budgets.md,sha256=6mihnbXUNxZCKRYguofu5y1ied5ytJ8d2pxMgdxMPWE,889
16
+ prettyplay/cache/.usages/storage.md,sha256=QlOU8Z5lQ8R8jBwBklNula79ERxtkftt5iP-KC6Cnuo,1320
17
+ prettyplay/config/CODEMANIFEST,sha256=xTA3y3pQ1zbvRVvpDRbHT4ASIze9-nZDmHYhbaWPf28,7912
18
+ prettyplay/config/__init__.py,sha256=I9vREkOLZpoWJnk36OlrEgyCPmlgTgTBGTWSXG1mhPU,241
19
+ prettyplay/config/loader.py,sha256=MGHEDJg6CMMhrqIQXIEuO_C0Xsc8VCT6bwxyfnvtJCE,7065
20
+ prettyplay/config/models.py,sha256=hM8sJAxxHXn11Crx-3dZ0VpBWi8E6yh6by-iZPfSwa4,3658
21
+ prettyplay/config/.usages/configuration.md,sha256=dWmyTyrYAgtBD4_ytzKJIbRK0HNuZzqmAsZxTp7pj5c,3978
22
+ prettyplay/driver/CODEMANIFEST,sha256=GferPW_EXtC_-04vqqBGYaCktskfhAwvVjZvHK2Ac9o,9430
23
+ prettyplay/driver/__init__.py,sha256=AkT-oLEjL_xb9Xi-h_Qvx7oHzkeyiAgm4dowMmo3PKk,226
24
+ prettyplay/driver/page.py,sha256=lI1JkOh6KD0Lj-6z2dw8y-SgDzrTaDACTi0_Swukpzg,12365
25
+ prettyplay/driver/session.py,sha256=V-2nI_EKuxv2ygmGw8Pthqh2UW9qC8btQsP7oxlegfE,10506
26
+ prettyplay/driver/.usages/facade.md,sha256=ZVEmIBJY4LXSUmMr16PnQCtGs-fdr5dDWtHCSz1EqwI,3376
27
+ prettyplay/engine/CODEMANIFEST,sha256=I6ZX1sle9z58T_ncD4kJfUrYiFTGZ-1F-v5Wa_9-JsM,14059
28
+ prettyplay/engine/__init__.py,sha256=4TXAo9CJxkg1MgcE5IuuP8yFI980tPn4orEPf9ZUMeU,333
29
+ prettyplay/engine/classification.py,sha256=MGTgWKIHtH6DXPGZ2LGhqr2neYJ4j-av2SY2_5R0Mh4,2704
30
+ prettyplay/engine/execution.py,sha256=L8OmLw8oeoaOO2Jg_lL-9WuEvntO6qWsGp7eDDxvJcc,1076
31
+ prettyplay/engine/generator.py,sha256=K_VFJqWklCNt17dhXMFDsBE8p7FUvzlGS9a23HmZX50,14469
32
+ prettyplay/engine/healer.py,sha256=5lf8NEunNQY7499n2XmUf9m2zMy_05GiAhwqAFcfYko,5212
33
+ prettyplay/engine/text.py,sha256=OhS7oR1wbUfMYk74GKMV7AtWIN5BcZ9fxNgq1SRorOg,718
34
+ prettyplay/engine/.usages/generation.md,sha256=80rON1oqAAu2U--fJd2qDYcpmGmLVg2oAjW7KkS9Wmc,2865
35
+ prettyplay/engine/.usages/healing.md,sha256=-rV17c7udKUx1WTVJgm8XzkDuM42nwvNs2YVNDjLlmM,1591
36
+ prettyplay/failures/CODEMANIFEST,sha256=OAg5FA0Wz9wUMAYt058TiEsvEoTCb30G0USQFt92cPg,5998
37
+ prettyplay/failures/__init__.py,sha256=nUhcscgBJsPZroIabB8_pFJxAgBBU6DPHNzp85C1CzI,361
38
+ prettyplay/failures/errors.py,sha256=hJz8czDaClKpeY6pwZbxerZB0UrmkKVv3iCVAqtF6Zw,5222
39
+ prettyplay/failures/.usages/taxonomy.md,sha256=gpi8v4UEqtOnFrigvoswIOL3QnSBtKKs2uKUEBnQbUo,2483
40
+ prettyplay/llm/CODEMANIFEST,sha256=jhsBkhi8EXRLspX8Y1ErTLylVG09irz1SaRBY1Y7HWE,7235
41
+ prettyplay/llm/__init__.py,sha256=BwjZpXIc4K11GuVw4UDgQAgkihqb7kYSS7eIOGFlt8A,377
42
+ prettyplay/llm/_request.py,sha256=F-iLnGyEdlwL0PnqMFCRZukiW4kwv2DCFEHEEExc5KE,7528
43
+ prettyplay/llm/anthropic_provider.py,sha256=V-OPAk8uCcDOHq5vkfqZMm4ne8L0EP0GmaNALZuMFOE,8349
44
+ prettyplay/llm/models.py,sha256=26aM3-JJzkJj9t5PXRi9RbmXXyDwWKvwZb2bW8N8gyA,638
45
+ prettyplay/llm/openai_provider.py,sha256=rjG6UJi8ljQecjQpIHjpPz932NTpnXYQY90z_Ruy9x0,7420
46
+ prettyplay/llm/provider.py,sha256=jjtB5YVO1w5FyYMdY0pPFiQJ8JtgNlO3St2ymZjrHbs,4714
47
+ prettyplay/llm/.usages/classification.md,sha256=Ue-q8PZoJndKS0BIpea0iMTfZSCk9LCEhVPaOFyH0lA,994
48
+ prettyplay/llm/.usages/providers.md,sha256=EJ6lk-imG0WmOPq3wXprvVYYI-0S3P5w4GPkzBFhDzw,1540
49
+ prettyplay/reporting/CODEMANIFEST,sha256=D2zwoqvIg-ZXXzt_rozjFjvpd0LBPXtbiict-9P_akY,4153
50
+ prettyplay/reporting/__init__.py,sha256=_dmLcKn7WDDqBWNAHT0VTRHPZu2-nrMpfux60gASTU0,187
51
+ prettyplay/reporting/hooks.py,sha256=KqnmYgvy_2oZv3swSCCOL7hLkA5UI17cOaifqWSCgxU,1867
52
+ prettyplay/reporting/reporter.py,sha256=TlOdgCIrmUZCobxAhUzmq1qs2ycjiNMR9HOaiwfxFXM,2325
53
+ prettyplay/reporting/.usages/hooks.md,sha256=u5I0vExr-1CoNXUz7J7yVOfugAIhaKSFEYFS7Ou5NUg,2315
54
+ prettyplay-0.0.0.dist-info/licenses/LICENSE,sha256=3fteeL9wFLa2P6MykXx6RkApbO0N9NUrSs9rVH8gxnc,1493
55
+ prettyplay-0.0.0.dist-info/METADATA,sha256=N-jueIY0iyqXYHzNUCBNlKS-_L2FclcGJLQFwRdPfCM,9466
56
+ prettyplay-0.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
57
+ prettyplay-0.0.0.dist-info/top_level.txt,sha256=ZByeTYDEYyWHvSGaI4cQYCrh1oiDpK5bTfnwv_ogWUg,11
58
+ prettyplay-0.0.0.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