<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Chapter 02.10 — Infrastructure on Ocarina — manual</title>
    <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/</link>
    <description>Recent content in Chapter 02.10 — Infrastructure on Ocarina — manual</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    <lastBuildDate>Mon, 01 Jun 2026 19:27:53 +0200</lastBuildDate>
    <atom:link href="https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>02.10.01 — WebDriversPool[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/01-drivers-pool/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/01-drivers-pool/</guid>
      <description>&lt;h1 id=&#34;021001webdriverspooldriver&#34;&gt;02.10.01 — &lt;code&gt;WebDriversPool[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021001webdriverspooldriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/infra/drivers_pool.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/drivers_pool.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Thread-safe driver pool, max concurrency enforced by semaphore, monitored async &lt;em&gt;warmup&lt;/em&gt;, clean &lt;em&gt;shutdown&lt;/em&gt;. No reuse — every &lt;code&gt;acquire()&lt;/code&gt; disposes its driver on the way out (clean state).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;constructor&#34;&gt;Constructor&lt;a class=&#34;anchor&#34; href=&#34;#constructor&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;WebDriversPool&lt;/span&gt;[Driver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        create_driver: Thunk[BuiltWebDriver[Driver]],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        max_size: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        warmup_timeout: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;float&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_create_driver &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_driver&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_pool: Queue[BuiltWebDriver[Driver]] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Queue(max_size)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_semaphore &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Semaphore(max_size)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_warmup_timeout &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            warmup_timeout &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; warmup_timeout &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;and&lt;/span&gt; warmup_timeout &lt;span style=&#34;color:#ff79c6&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;0.1&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;60.0&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;5&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Primitive&lt;/th&gt;&#xA;          &lt;th&gt;Role&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;Queue(max_size)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Queue of &lt;em&gt;pre-built&lt;/em&gt; available drivers.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;Semaphore(max_size)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Guarantees that &lt;strong&gt;at most &lt;code&gt;max_size&lt;/code&gt; drivers&lt;/strong&gt; live simultaneously (whether queued or acquired).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;warmup_timeout&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Max no-progress delay during warmup before raising &lt;code&gt;WarmupTimeoutError&lt;/code&gt;. Default 300s.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;acquire&#34;&gt;&lt;code&gt;acquire()&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#acquire&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@contextmanager&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;acquire&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Iterator[Driver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;try&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        driver, dispose &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_pool&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;get_nowait()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;except&lt;/span&gt; Empty:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_semaphore&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;acquire()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;try&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            driver, dispose &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_create_driver()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;except&lt;/span&gt; Exception:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_semaphore&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;release()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;raise&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;try&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;yield&lt;/span&gt; driver&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;finally&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;with&lt;/span&gt; suppress(Exception):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            dispose()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_semaphore&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;release()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;              acquire()&#xA;                  │&#xA;                  ▼&#xA;   ┌──────────────────────────────────┐&#xA;   │ pool.get_nowait()                │── OK ─► driver, dispose         ← warmup case&#xA;   └──────────────┬───────────────────┘&#xA;                  │ Empty&#xA;                  ▼&#xA;   ┌──────────────────────────────────┐&#xA;   │ sem.acquire()                    │  ← blocks if N drivers alive&#xA;   └──────────────┬───────────────────┘&#xA;                  ▼&#xA;   ┌──────────────────────────────────┐&#xA;   │ create_driver()                  │── raises ─► sem.release(); raise&#xA;   └──────────────┬───────────────────┘&#xA;                  ▼&#xA;   ┌──────────────────────────────────┐&#xA;   │ yield driver                     │  ← caller uses it&#xA;   └──────────────┬───────────────────┘&#xA;                  ▼&#xA;                finally :&#xA;                  with suppress : dispose()       ← driver disposed&#xA;                  sem.release()                   ← release a slot&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;1-no-reuse&#34;&gt;1. No reuse&lt;a class=&#34;anchor&#34; href=&#34;#1-no-reuse&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;&lt;code&gt;Queue.get_nowait()&lt;/code&gt; &lt;em&gt;consumes&lt;/em&gt; the entry. Once pulled, the driver never goes back. At the end (&lt;code&gt;finally&lt;/code&gt;), it&amp;rsquo;s &lt;strong&gt;disposed&lt;/strong&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.02 — DriverBuilder[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/02-driver-builder/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/02-driver-builder/</guid>
      <description>&lt;h1 id=&#34;021002driverbuilderdriver&#34;&gt;02.10.02 — &lt;code&gt;DriverBuilder[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021002driverbuilderdriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/infra/driver_builder.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/driver_builder.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Encapsulates &lt;strong&gt;profile&lt;/strong&gt; handling (often a tmp copy of a user directory) and produces the &lt;code&gt;(driver, dispose)&lt;/code&gt; pair the pool expects.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;why-a-builder&#34;&gt;Why a builder?&lt;a class=&#34;anchor&#34; href=&#34;#why-a-builder&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Building a Selenium driver with a &lt;em&gt;profile&lt;/em&gt; requires:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;Copy the profile&lt;/strong&gt; to a temp folder (otherwise Firefox/Chrome locks the original).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Start the driver&lt;/strong&gt; pointing at the temp folder.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;At the end&lt;/strong&gt;: &lt;code&gt;driver.quit()&lt;/code&gt;, then delete the temp folder.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;&lt;code&gt;DriverBuilder&lt;/code&gt; factors out those three steps:&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.03 — Screenshotter[TDriver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/03-screenshotter/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/03-screenshotter/</guid>
      <description>&lt;h1 id=&#34;021003screenshottertdriver&#34;&gt;02.10.03 — &lt;code&gt;Screenshotter[TDriver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021003screenshottertdriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source file: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/infra/screenshotter.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/screenshotter.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Generic, &lt;em&gt;thread-safe&lt;/em&gt; screenshot utility. Driver-agnostic via a &lt;code&gt;Protocol&lt;/code&gt;, configurable via a &lt;code&gt;dataclass&lt;/code&gt;. Supports burst.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;protocol&#34;&gt;&lt;code&gt;Protocol&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#protocol&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;ScreenshotDriver&lt;/span&gt;(Protocol):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;save_screenshot&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;, path: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Save standard viewport screenshot.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. &lt;em&gt;Any object&lt;/em&gt; with &lt;code&gt;save_screenshot(path: str) -&amp;gt; bool&lt;/code&gt; qualifies. Selenium WebDriver has it natively (&lt;code&gt;driver.save_screenshot&lt;/code&gt;), Playwright can be wrapped, a fake driver in tests exposes it trivially.&lt;/p&gt;&#xA;&lt;h2 id=&#34;screenshotterconfigtdriver&#34;&gt;&lt;code&gt;ScreenshotterConfig[TDriver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#screenshotterconfigtdriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@dataclass(frozen&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;ScreenshotterConfig&lt;/span&gt;[TDriver: ScreenshotDriver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    output_dir: Path&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    file_ext: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;.png&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    health_check: HealthCheck[TDriver] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    save_full_page: SaveFullPageScreenshot[TDriver] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    default_burst_delay: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;float&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;0.5&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    max_filename_retries: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;500&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    uuid_length: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;8&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Field&lt;/th&gt;&#xA;          &lt;th&gt;Type&lt;/th&gt;&#xA;          &lt;th&gt;Role&lt;/th&gt;&#xA;          &lt;th&gt;Default&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;output_dir&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Path&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Screenshot directory&lt;/td&gt;&#xA;          &lt;td&gt; — &lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;file_ext&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Extension (&lt;code&gt;.png&lt;/code&gt;, &lt;code&gt;.jpg&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;.png&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;health_check&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;HealthCheck[TDriver] | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;(driver) -&amp;gt; None&lt;/code&gt; that raises if the driver is dead&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;save_full_page&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;SaveFullPageScreenshot[TDriver] | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;(driver, path) -&amp;gt; bool&lt;/code&gt; for full-page screenshots (Firefox only)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;default_burst_delay&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;float&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Delay between 2 shots in burst mode&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;0.5&lt;/code&gt;s&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;max_filename_retries&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Max attempts to generate a unique name&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;500&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;uuid_length&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;int&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;UUID suffix length in the name&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;8&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Immutable. Configure once, ship.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.04 — ActCounter &#43; ThreadsBasedActCounter</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/04-act-counter/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/04-act-counter/</guid>
      <description>&lt;h1 id=&#34;021004actcounter--threadsbasedactcounter&#34;&gt;02.10.04 — &lt;code&gt;ActCounter&lt;/code&gt; + &lt;code&gt;ThreadsBasedActCounter&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#021004actcounter--threadsbasedactcounter&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Thread-local counter of &lt;code&gt;act&lt;/code&gt; calls per attempt. Lets the report say &amp;ldquo;&lt;em&gt;this test made 17 steps before dying at step 18&lt;/em&gt;.&amp;rdquo;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;interface&#34;&gt;Interface&lt;a class=&#34;anchor&#34; href=&#34;#interface&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/ocarina/infra/act_counter.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;ActCounter&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;get&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;reset&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;incr_act_call_count&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;: &lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;default-implementation&#34;&gt;Default implementation&lt;a class=&#34;anchor&#34; href=&#34;#default-implementation&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/ocarina/opinionated/infra/act_counter.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;from&lt;/span&gt; threading &lt;span style=&#34;color:#ff79c6&#34;&gt;import&lt;/span&gt; local&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_thread_local &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; local()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;_COUNTER_KEY: Final[&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt;] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;ocarina_counter&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;ActCounter&lt;/span&gt;(_ActCounter):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;get&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;int&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;getattr&lt;/span&gt;(_thread_local, _COUNTER_KEY, &lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;reset&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;setattr&lt;/span&gt;(_thread_local, _COUNTER_KEY, &lt;span style=&#34;color:#bd93f9&#34;&gt;0&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;incr_act_call_count&lt;/span&gt;(&lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;hasattr&lt;/span&gt;(_thread_local, _COUNTER_KEY):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;reset()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;setattr&lt;/span&gt;(_thread_local, _COUNTER_KEY, &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;getattr&lt;/span&gt;(_thread_local, _COUNTER_KEY) &lt;span style=&#34;color:#ff79c6&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#bd93f9&#34;&gt;1&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;threading.local()&lt;/code&gt;&lt;/strong&gt;: namespace where attributes are &lt;em&gt;per thread&lt;/em&gt;. Each thread has its own value.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;_COUNTER_KEY = &amp;quot;ocarina_counter&amp;quot;&lt;/code&gt;&lt;/strong&gt;: attribute key.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;No lock&lt;/strong&gt;: each thread only writes its own value, so no race.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;why-thread-local-rather-than-a-shared-counter--lock&#34;&gt;Why thread-local rather than a shared counter + lock?&lt;a class=&#34;anchor&#34; href=&#34;#why-thread-local-rather-than-a-shared-counter--lock&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Ocarina&amp;rsquo;s architecture says &lt;strong&gt;one thread = one test&lt;/strong&gt;. We&amp;rsquo;ve got no appetite for a whole &lt;em&gt;state monad&lt;/em&gt; — might as well park this &amp;ldquo;impurity&amp;rdquo; (still a piece of state) right here. That&amp;rsquo;s it.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.05 — Selenium adapters</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/05-selenium-adapters/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/05-selenium-adapters/</guid>
      <description>&lt;h1 id=&#34;021005selenium-adapters&#34;&gt;02.10.05 — Selenium adapters&lt;a class=&#34;anchor&#34; href=&#34;#021005selenium-adapters&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source folder: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/tree/main/src/ocarina/infra/selenium&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/selenium/&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Everything that materializes the abstractions (&lt;code&gt;POMBase&lt;/code&gt;, &lt;code&gt;WebDriversPool&lt;/code&gt;, &lt;code&gt;Screenshotter&lt;/code&gt;) in Selenium flavor. &lt;strong&gt;Lives outside the pure DSL&lt;/strong&gt; — and since &lt;code&gt;1.1.3&lt;/code&gt; it has a twin: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/tree/main/src/ocarina/infra/playwright&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/playwright/&lt;/code&gt;&lt;/a&gt; ships the exact same contracts in Playwright flavor. Swapping backend is now picking a folder, not writing one.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;files-in-the-folder&#34;&gt;Files in the folder&lt;a class=&#34;anchor&#34; href=&#34;#files-in-the-folder&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;File&lt;/th&gt;&#xA;          &lt;th&gt;Role&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_driver.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;_build_firefox&lt;/code&gt;, &lt;code&gt;_build_chrome&lt;/code&gt;, &lt;code&gt;_build_edge&lt;/code&gt;, &lt;code&gt;_build_safari&lt;/code&gt; + dispatch&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_drivers_pool.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_selenium_drivers_pool&lt;/code&gt; (uses &lt;code&gt;DriverBuilder&lt;/code&gt; + &lt;code&gt;WebDriversPool&lt;/code&gt;)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_screenshotter.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;create_selenium_screenshotter&lt;/code&gt; + &lt;code&gt;_selenium_save_full_page&lt;/code&gt; (Firefox-only)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;driver_healthcheck.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;driver_healthcheck(driver)&lt;/code&gt; — pings &lt;code&gt;driver.title&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;mixins.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;SeleniumTitleMixin&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;create_driverpy&#34;&gt;&lt;code&gt;create_driver.py&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#create_driverpy&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_build_firefox&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, profile_path, driver_path, headless, wait_timeout) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; WebDriver:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    service &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; FirefoxService(executable_path&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver_path)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    options &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; FirefoxOptions()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; headless:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        options&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;add_argument(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;-headless&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; profile_path:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        options&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;profile &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; FirefoxProfile(profile_path)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    driver &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Firefox(service&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;service, options&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;options)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    driver&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;implicitly_wait(wait_timeout)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; driver&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_build_chrome&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, profile_path, driver_path, headless, wait_timeout) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; WebDriver:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    service &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; ChromeService(executable_path&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver_path)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    options &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; ChromeOptions()&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; headless:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        options&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;add_argument(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;--headless=new&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; profile_path:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        options&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;add_argument(&lt;span style=&#34;color:#f1fa8c&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;--user-data-dir=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;{&lt;/span&gt;profile_path&lt;span style=&#34;color:#f1fa8c&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    driver &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Chrome(service&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;service, options&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;options)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    driver&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;implicitly_wait(wait_timeout)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; driver&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_build_edge&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, profile_path, driver_path, headless, wait_timeout) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; WebDriver:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;# … same as Chrome with EdgeService/EdgeOptions/Edge …&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;_build_safari&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, profile_path, driver_path, headless, wait_timeout) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; WebDriver:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;# … Safari supports neither driver_path (uses macOS&amp;#39; native safaridriver),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;#   nor profile_path (no custom profile), nor headless …&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;1-implicitly_wait-set-only-once&#34;&gt;1. &lt;code&gt;implicitly_wait&lt;/code&gt; set only once&lt;a class=&#34;anchor&#34; href=&#34;#1-implicitly_wait-set-only-once&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Each builder calls &lt;code&gt;driver.implicitly_wait(wait_timeout)&lt;/code&gt; &lt;em&gt;once&lt;/em&gt;. Any &lt;code&gt;find_element&lt;/code&gt; that doesn&amp;rsquo;t find the element immediately waits up to &lt;code&gt;wait_timeout&lt;/code&gt; seconds before raising &lt;code&gt;NoSuchElementException&lt;/code&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.10.06 — The Playwright actor: one owner thread</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/06-playwright-actor/</link>
      <pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/02-ocarina/10-infra/06-playwright-actor/</guid>
      <description>&lt;h1 id=&#34;021006the-playwright-actor-one-owner-thread&#34;&gt;02.10.06 — The Playwright actor: one owner thread&lt;a class=&#34;anchor&#34; href=&#34;#021006the-playwright-actor-one-owner-thread&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Source folder: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/tree/main/src/ocarina/infra/playwright&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/infra/playwright/&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;The Playwright adapter mirrors the Selenium one file for file (&lt;code&gt;create_driver&lt;/code&gt;, &lt;code&gt;create_drivers_pool&lt;/code&gt;, &lt;code&gt;create_screenshotter&lt;/code&gt;, &lt;code&gt;driver_healthcheck&lt;/code&gt;, &lt;code&gt;mixins&lt;/code&gt;), with &lt;strong&gt;one extra file&lt;/strong&gt;: &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/infra/playwright/driver.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;driver.py&lt;/code&gt;&lt;/a&gt;. That file deserves a chapter of its own. It is what reconciles Playwright&amp;rsquo;s &lt;em&gt;sync&lt;/em&gt; API — inherently bound to one thread — with Ocarina&amp;rsquo;s threaded model (pool, warmup, Watcher). The model has been stressed and stabilized; it holds.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;the-problem-playwrights-sync-api-is-thread-affine&#34;&gt;The problem: Playwright&amp;rsquo;s sync API is thread-affine&lt;a class=&#34;anchor&#34; href=&#34;#the-problem-playwrights-sync-api-is-thread-affine&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Playwright&amp;rsquo;s &lt;strong&gt;sync&lt;/strong&gt; API binds every object it hands out — &lt;code&gt;Playwright&lt;/code&gt;, &lt;code&gt;Browser&lt;/code&gt;, &lt;code&gt;BrowserContext&lt;/code&gt;, &lt;code&gt;Page&lt;/code&gt;, &lt;code&gt;Locator&lt;/code&gt;, … — to the thread that called &lt;code&gt;sync_playwright().start()&lt;/code&gt;. Under the hood it is a &lt;em&gt;greenlet&lt;/em&gt; pinned to that thread. Touching any of them from another thread raises:&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
