alphawindow 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ recursive-include src/alphawindow py.typed
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: alphawindow
3
+ Version: 0.1.0
4
+ Summary: Windows-first window automation abstractions with pluggable input and output modes.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Provides-Extra: native
8
+ Requires-Dist: pywin32>=306; platform_system == "Windows" and extra == "native"
9
+ Requires-Dist: pynput>=1.7.6; platform_system == "Windows" and extra == "native"
10
+ Provides-Extra: test
11
+ Requires-Dist: pytest>=8; extra == "test"
12
+
13
+ # AlphaWindow
14
+
15
+ AlphaWindow is a Windows-first automation abstraction layer. It exposes one session API for window operations while allowing callers to choose different input and output modes.
16
+
17
+ The first implementation includes:
18
+
19
+ - Built-in I/O profiles for every declared input and output mode, including `inspect`, `observe`, `desktop_observe`, `dry_run`, `win32_message`, `message_only`, `uia_control`, `uia_visual`, `global_input`, `foreground_visual`, `guarded_input`, `guarded_desktop`, `isolated_observe`, `isolated_telemetry`, `virtual_window`, `virtual_telemetry`, and `virtual_input_only`.
20
+ - A `WindowSession` API for `prefetch`, `capture`, `observe`, `click`, `key_down`, `key_up`, and `type_text`.
21
+ - Generic delegation backends for message, UIA, foreground, guarded foreground, state, desktop-region, hook-telemetry, and no-output modes.
22
+ - Compatibility checks so a backend cannot silently fall back to a broader side-effect mode.
23
+ - Hook-aware `isolated` and `virtualized` input backends that accept pluggable hook managers and virtual input transports.
24
+
25
+ Native pywin32, pynput, and DLL hook adapters are intended to plug into the same protocols in later slices.
26
+
27
+ ## Install
28
+
29
+ From PyPI after publishing:
30
+
31
+ ```powershell
32
+ pip install alphawindow
33
+ ```
34
+
35
+ From this checkout:
36
+
37
+ ```powershell
38
+ pip install .
39
+ ```
40
+
41
+ ## Import
42
+
43
+ ```python
44
+ import alphawindow
45
+
46
+ from alphawindow import (
47
+ AutomationConfig,
48
+ InputMode,
49
+ OutputMode,
50
+ WindowSelector,
51
+ WindowSession,
52
+ get_profile,
53
+ )
54
+
55
+ print(alphawindow.__version__)
56
+ print(get_profile("observe").input_mode is InputMode.DRY_RUN)
57
+ ```
58
+
59
+ ## Release
60
+
61
+ Publishing is handled by GitHub Actions in `.github/workflows/publish.yml`.
62
+
63
+ 1. Configure PyPI Trusted Publishing for this repository, environment `pypi`, and workflow `publish.yml`.
64
+ 2. Update the version in `pyproject.toml` and `src/alphawindow/_version.py`.
65
+ 3. Push a version tag:
66
+
67
+ ```powershell
68
+ git tag v0.1.0
69
+ git push origin v0.1.0
70
+ ```
71
+
72
+ The workflow runs tests on Windows for Python 3.11, 3.12, and 3.13, builds the package, checks distribution metadata, and publishes to PyPI only for `v*` tags.
@@ -0,0 +1,60 @@
1
+ # AlphaWindow
2
+
3
+ AlphaWindow is a Windows-first automation abstraction layer. It exposes one session API for window operations while allowing callers to choose different input and output modes.
4
+
5
+ The first implementation includes:
6
+
7
+ - Built-in I/O profiles for every declared input and output mode, including `inspect`, `observe`, `desktop_observe`, `dry_run`, `win32_message`, `message_only`, `uia_control`, `uia_visual`, `global_input`, `foreground_visual`, `guarded_input`, `guarded_desktop`, `isolated_observe`, `isolated_telemetry`, `virtual_window`, `virtual_telemetry`, and `virtual_input_only`.
8
+ - A `WindowSession` API for `prefetch`, `capture`, `observe`, `click`, `key_down`, `key_up`, and `type_text`.
9
+ - Generic delegation backends for message, UIA, foreground, guarded foreground, state, desktop-region, hook-telemetry, and no-output modes.
10
+ - Compatibility checks so a backend cannot silently fall back to a broader side-effect mode.
11
+ - Hook-aware `isolated` and `virtualized` input backends that accept pluggable hook managers and virtual input transports.
12
+
13
+ Native pywin32, pynput, and DLL hook adapters are intended to plug into the same protocols in later slices.
14
+
15
+ ## Install
16
+
17
+ From PyPI after publishing:
18
+
19
+ ```powershell
20
+ pip install alphawindow
21
+ ```
22
+
23
+ From this checkout:
24
+
25
+ ```powershell
26
+ pip install .
27
+ ```
28
+
29
+ ## Import
30
+
31
+ ```python
32
+ import alphawindow
33
+
34
+ from alphawindow import (
35
+ AutomationConfig,
36
+ InputMode,
37
+ OutputMode,
38
+ WindowSelector,
39
+ WindowSession,
40
+ get_profile,
41
+ )
42
+
43
+ print(alphawindow.__version__)
44
+ print(get_profile("observe").input_mode is InputMode.DRY_RUN)
45
+ ```
46
+
47
+ ## Release
48
+
49
+ Publishing is handled by GitHub Actions in `.github/workflows/publish.yml`.
50
+
51
+ 1. Configure PyPI Trusted Publishing for this repository, environment `pypi`, and workflow `publish.yml`.
52
+ 2. Update the version in `pyproject.toml` and `src/alphawindow/_version.py`.
53
+ 3. Push a version tag:
54
+
55
+ ```powershell
56
+ git tag v0.1.0
57
+ git push origin v0.1.0
58
+ ```
59
+
60
+ The workflow runs tests on Windows for Python 3.11, 3.12, and 3.13, builds the package, checks distribution metadata, and publishes to PyPI only for `v*` tags.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "alphawindow"
7
+ version = "0.1.0"
8
+ description = "Windows-first window automation abstractions with pluggable input and output modes."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = []
12
+
13
+ [project.optional-dependencies]
14
+ native = [
15
+ "pywin32>=306; platform_system == 'Windows'",
16
+ "pynput>=1.7.6; platform_system == 'Windows'",
17
+ ]
18
+ test = [
19
+ "pytest>=8",
20
+ ]
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
24
+
25
+ [tool.setuptools.package-data]
26
+ alphawindow = ["py.typed"]
27
+
28
+ [tool.pytest.ini_options]
29
+ pythonpath = ["src"]
30
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from .backends import (
4
+ DelegatingInputBackend,
5
+ DelegatingOutputBackend,
6
+ DryRunInputBackend,
7
+ IsolatedInputBackend,
8
+ NullOutputBackend,
9
+ StateOutputBackend,
10
+ VirtualizedInputBackend,
11
+ )
12
+ from .profiles import BUILTIN_PROFILES, get_profile
13
+ from .session import WindowSession, open_window
14
+ from ._version import __version__
15
+ from .types import (
16
+ AutomationConfig,
17
+ AutomationProfile,
18
+ BackendCompatibilityError,
19
+ Capability,
20
+ InputMode,
21
+ Operation,
22
+ OperationResult,
23
+ OutputMode,
24
+ WindowNotFoundError,
25
+ WindowSelector,
26
+ WindowSnapshot,
27
+ )
28
+
29
+ __all__ = [
30
+ "AutomationConfig",
31
+ "AutomationProfile",
32
+ "BUILTIN_PROFILES",
33
+ "BackendCompatibilityError",
34
+ "Capability",
35
+ "DelegatingInputBackend",
36
+ "DelegatingOutputBackend",
37
+ "DryRunInputBackend",
38
+ "InputMode",
39
+ "IsolatedInputBackend",
40
+ "NullOutputBackend",
41
+ "Operation",
42
+ "OperationResult",
43
+ "OutputMode",
44
+ "StateOutputBackend",
45
+ "VirtualizedInputBackend",
46
+ "WindowNotFoundError",
47
+ "WindowSelector",
48
+ "WindowSession",
49
+ "WindowSnapshot",
50
+ "__version__",
51
+ "get_profile",
52
+ "open_window",
53
+ ]
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from .types import (
8
+ BackendCompatibilityError,
9
+ Capability,
10
+ HookManager,
11
+ InputBackend,
12
+ InputMode,
13
+ Operation,
14
+ OperationResult,
15
+ OutputMode,
16
+ VirtualInputTransport,
17
+ WindowSnapshot,
18
+ )
19
+
20
+
21
+ INPUT_MODE_CAPABILITIES: dict[InputMode, frozenset[Capability]] = {
22
+ InputMode.DRY_RUN: frozenset(),
23
+ InputMode.MESSAGE: frozenset({Capability.WINDOW_MESSAGE_INPUT}),
24
+ InputMode.UIA: frozenset({Capability.UIA_INPUT}),
25
+ InputMode.FOREGROUND: frozenset({Capability.GLOBAL_INPUT}),
26
+ InputMode.GUARDED_FOREGROUND: frozenset({Capability.GUARDED_GLOBAL_INPUT}),
27
+ InputMode.ISOLATED: frozenset({Capability.HOOKED_INPUT}),
28
+ InputMode.VIRTUALIZED: frozenset({Capability.HOOKED_INPUT, Capability.VIRTUAL_INPUT}),
29
+ }
30
+
31
+ OUTPUT_MODE_CAPABILITIES: dict[OutputMode, frozenset[Capability]] = {
32
+ OutputMode.STATE: frozenset({Capability.WINDOW_STATE}),
33
+ OutputMode.WINDOW_CAPTURE: frozenset({Capability.WINDOW_CAPTURE}),
34
+ OutputMode.DESKTOP_REGION: frozenset({Capability.DESKTOP_CAPTURE}),
35
+ OutputMode.HOOK_TELEMETRY: frozenset({Capability.HOOK_TELEMETRY}),
36
+ OutputMode.NONE: frozenset(),
37
+ }
38
+
39
+
40
+ class DryRunInputBackend:
41
+ mode = InputMode.DRY_RUN
42
+ capabilities = frozenset()
43
+
44
+ def __init__(self) -> None:
45
+ self.operations: list[Operation] = []
46
+
47
+ def perform(self, target: WindowSnapshot, operation: Operation) -> OperationResult:
48
+ self.operations.append(operation)
49
+ return OperationResult(
50
+ operation=operation,
51
+ executed=False,
52
+ backend="dry_run",
53
+ details={"hwnd": target.hwnd},
54
+ )
55
+
56
+
57
+ @dataclass
58
+ class DelegatingInputBackend:
59
+ mode: InputMode
60
+ performer: Callable[[WindowSnapshot, Operation], OperationResult | None]
61
+ name: str | None = None
62
+ capabilities: frozenset[Capability] | None = None
63
+
64
+ def __post_init__(self) -> None:
65
+ if self.mode in {InputMode.ISOLATED, InputMode.VIRTUALIZED}:
66
+ raise BackendCompatibilityError(
67
+ "use dedicated hook-aware backends for isolated and virtualized input"
68
+ )
69
+ if self.capabilities is None:
70
+ self.capabilities = INPUT_MODE_CAPABILITIES[self.mode]
71
+ if self.name is None:
72
+ self.name = self.mode.value
73
+
74
+ def perform(self, target: WindowSnapshot, operation: Operation) -> OperationResult:
75
+ result = self.performer(target, operation)
76
+ if isinstance(result, OperationResult):
77
+ return result
78
+ return OperationResult(
79
+ operation=operation,
80
+ executed=True,
81
+ backend=str(self.name),
82
+ details={"hwnd": target.hwnd},
83
+ )
84
+
85
+
86
+ class StateOutputBackend:
87
+ mode = OutputMode.STATE
88
+ capabilities = frozenset({Capability.WINDOW_STATE})
89
+
90
+ def capture(self, target: WindowSnapshot) -> WindowSnapshot:
91
+ return target
92
+
93
+ def observe(self, target: WindowSnapshot) -> WindowSnapshot:
94
+ return target
95
+
96
+
97
+ class NullOutputBackend:
98
+ mode = OutputMode.NONE
99
+ capabilities = frozenset()
100
+
101
+ def capture(self, target: WindowSnapshot) -> None:
102
+ return None
103
+
104
+ def observe(self, target: WindowSnapshot) -> WindowSnapshot:
105
+ return target
106
+
107
+
108
+ @dataclass
109
+ class DelegatingOutputBackend:
110
+ mode: OutputMode
111
+ capture_fn: Callable[[WindowSnapshot], Any] | None = None
112
+ observe_fn: Callable[[WindowSnapshot], WindowSnapshot] | None = None
113
+ name: str | None = None
114
+ capabilities: frozenset[Capability] | None = None
115
+
116
+ def __post_init__(self) -> None:
117
+ if self.capabilities is None:
118
+ self.capabilities = OUTPUT_MODE_CAPABILITIES[self.mode]
119
+ if self.name is None:
120
+ self.name = self.mode.value
121
+
122
+ def capture(self, target: WindowSnapshot) -> Any:
123
+ if self.capture_fn is None:
124
+ if self.mode is OutputMode.NONE:
125
+ return None
126
+ if self.mode is OutputMode.STATE:
127
+ return target
128
+ raise BackendCompatibilityError(
129
+ f"output backend {self.name!r} has no capture function"
130
+ )
131
+ return self.capture_fn(target)
132
+
133
+ def observe(self, target: WindowSnapshot) -> WindowSnapshot:
134
+ if self.observe_fn is None:
135
+ return target
136
+ return self.observe_fn(target)
137
+
138
+
139
+ @dataclass
140
+ class IsolatedInputBackend:
141
+ hook_manager: HookManager
142
+ delegate: InputBackend | None = None
143
+
144
+ mode = InputMode.ISOLATED
145
+ capabilities = frozenset({Capability.HOOKED_INPUT})
146
+
147
+ def __post_init__(self) -> None:
148
+ if self.hook_manager is None:
149
+ raise BackendCompatibilityError("isolated input requires a hook manager")
150
+
151
+ def perform(self, target: WindowSnapshot, operation: Operation) -> OperationResult:
152
+ self.hook_manager.ensure_injected(target)
153
+ if self.delegate is not None:
154
+ return self.delegate.perform(target, operation)
155
+ return OperationResult(
156
+ operation=operation,
157
+ executed=False,
158
+ backend="isolated",
159
+ details={"hwnd": target.hwnd, "hooked": True},
160
+ )
161
+
162
+
163
+ @dataclass
164
+ class VirtualizedInputBackend:
165
+ hook_manager: HookManager
166
+ transport: VirtualInputTransport
167
+
168
+ mode = InputMode.VIRTUALIZED
169
+ capabilities = frozenset({Capability.HOOKED_INPUT, Capability.VIRTUAL_INPUT})
170
+
171
+ def __post_init__(self) -> None:
172
+ if self.hook_manager is None:
173
+ raise BackendCompatibilityError("virtualized input requires a hook manager")
174
+ if self.transport is None:
175
+ raise BackendCompatibilityError("virtualized input requires a transport")
176
+
177
+ def perform(self, target: WindowSnapshot, operation: Operation) -> OperationResult:
178
+ self.hook_manager.ensure_injected(target)
179
+ return self.transport.send(target, operation)
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ from .types import (
4
+ AutomationProfile,
5
+ BackendCompatibilityError,
6
+ InputMode,
7
+ OutputMode,
8
+ )
9
+
10
+
11
+ BUILTIN_PROFILES: dict[str, AutomationProfile] = {
12
+ "inspect": AutomationProfile(
13
+ name="inspect",
14
+ input_mode=InputMode.DRY_RUN,
15
+ output_mode=OutputMode.STATE,
16
+ description="Inspect window state without capture or input.",
17
+ ),
18
+ "observe": AutomationProfile(
19
+ name="observe",
20
+ input_mode=InputMode.DRY_RUN,
21
+ output_mode=OutputMode.WINDOW_CAPTURE,
22
+ description="Capture or inspect a window without sending input.",
23
+ ),
24
+ "desktop_observe": AutomationProfile(
25
+ name="desktop_observe",
26
+ input_mode=InputMode.DRY_RUN,
27
+ output_mode=OutputMode.DESKTOP_REGION,
28
+ description="Observe a target through desktop-region capture.",
29
+ ),
30
+ "dry_run": AutomationProfile(
31
+ name="dry_run",
32
+ input_mode=InputMode.DRY_RUN,
33
+ output_mode=OutputMode.NONE,
34
+ description="Plan operations without output or input side effects.",
35
+ ),
36
+ "win32_message": AutomationProfile(
37
+ name="win32_message",
38
+ input_mode=InputMode.MESSAGE,
39
+ output_mode=OutputMode.WINDOW_CAPTURE,
40
+ description="Send window-message input while capturing the target window.",
41
+ ),
42
+ "message_only": AutomationProfile(
43
+ name="message_only",
44
+ input_mode=InputMode.MESSAGE,
45
+ output_mode=OutputMode.NONE,
46
+ description="Send window-message input without output capture.",
47
+ ),
48
+ "uia_control": AutomationProfile(
49
+ name="uia_control",
50
+ input_mode=InputMode.UIA,
51
+ output_mode=OutputMode.STATE,
52
+ description="Drive standard controls through Windows UI Automation.",
53
+ ),
54
+ "uia_visual": AutomationProfile(
55
+ name="uia_visual",
56
+ input_mode=InputMode.UIA,
57
+ output_mode=OutputMode.WINDOW_CAPTURE,
58
+ description="Drive UI Automation controls while capturing the window.",
59
+ ),
60
+ "global_input": AutomationProfile(
61
+ name="global_input",
62
+ input_mode=InputMode.FOREGROUND,
63
+ output_mode=OutputMode.DESKTOP_REGION,
64
+ description="Use global foreground input such as pynput or SendInput.",
65
+ ),
66
+ "foreground_visual": AutomationProfile(
67
+ name="foreground_visual",
68
+ input_mode=InputMode.FOREGROUND,
69
+ output_mode=OutputMode.WINDOW_CAPTURE,
70
+ description="Use global foreground input with direct window capture.",
71
+ ),
72
+ "guarded_input": AutomationProfile(
73
+ name="guarded_input",
74
+ input_mode=InputMode.GUARDED_FOREGROUND,
75
+ output_mode=OutputMode.WINDOW_CAPTURE,
76
+ description="Use foreground input with cleanup and focus restoration.",
77
+ ),
78
+ "guarded_desktop": AutomationProfile(
79
+ name="guarded_desktop",
80
+ input_mode=InputMode.GUARDED_FOREGROUND,
81
+ output_mode=OutputMode.DESKTOP_REGION,
82
+ description="Use guarded foreground input with desktop-region capture.",
83
+ ),
84
+ "isolated_observe": AutomationProfile(
85
+ name="isolated_observe",
86
+ input_mode=InputMode.ISOLATED,
87
+ output_mode=OutputMode.WINDOW_CAPTURE,
88
+ requires_hook=True,
89
+ description="Inject an isolation hook before any optional delegated input.",
90
+ ),
91
+ "isolated_telemetry": AutomationProfile(
92
+ name="isolated_telemetry",
93
+ input_mode=InputMode.ISOLATED,
94
+ output_mode=OutputMode.HOOK_TELEMETRY,
95
+ requires_hook=True,
96
+ description="Inject an isolation hook and read hook telemetry.",
97
+ ),
98
+ "virtual_window": AutomationProfile(
99
+ name="virtual_window",
100
+ input_mode=InputMode.VIRTUALIZED,
101
+ output_mode=OutputMode.WINDOW_CAPTURE,
102
+ requires_hook=True,
103
+ description="Route virtual mouse and keyboard operations through a hook transport.",
104
+ ),
105
+ "virtual_telemetry": AutomationProfile(
106
+ name="virtual_telemetry",
107
+ input_mode=InputMode.VIRTUALIZED,
108
+ output_mode=OutputMode.HOOK_TELEMETRY,
109
+ requires_hook=True,
110
+ description="Route virtual input and read hook telemetry.",
111
+ ),
112
+ "virtual_input_only": AutomationProfile(
113
+ name="virtual_input_only",
114
+ input_mode=InputMode.VIRTUALIZED,
115
+ output_mode=OutputMode.NONE,
116
+ requires_hook=True,
117
+ description="Route virtual input without output capture.",
118
+ ),
119
+ }
120
+
121
+
122
+ def get_profile(profile: str | AutomationProfile) -> AutomationProfile:
123
+ if isinstance(profile, AutomationProfile):
124
+ return profile
125
+ try:
126
+ return BUILTIN_PROFILES[profile]
127
+ except KeyError as exc:
128
+ raise BackendCompatibilityError(
129
+ f"unknown automation profile: {profile}"
130
+ ) from exc
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+ from .profiles import get_profile
7
+ from .types import (
8
+ AutomationConfig,
9
+ AutomationProfile,
10
+ BackendCompatibilityError,
11
+ Capability,
12
+ InputBackend,
13
+ InputMode,
14
+ Operation,
15
+ OperationResult,
16
+ OutputBackend,
17
+ WindowNotFoundError,
18
+ WindowResolver,
19
+ WindowSelector,
20
+ WindowSnapshot,
21
+ )
22
+
23
+
24
+ class WindowSession:
25
+ def __init__(
26
+ self,
27
+ *,
28
+ selector: WindowSelector,
29
+ resolver: WindowResolver,
30
+ input_backend: InputBackend,
31
+ output_backend: OutputBackend,
32
+ config: AutomationConfig | None = None,
33
+ ) -> None:
34
+ self.selector = selector
35
+ self.resolver = resolver
36
+ self.input_backend = input_backend
37
+ self.output_backend = output_backend
38
+ self.config = config or AutomationConfig()
39
+ self.profile: AutomationProfile = get_profile(self.config.profile)
40
+ self._prefetched: tuple[float, WindowSnapshot] | None = None
41
+ self._validate_backends()
42
+
43
+ def prefetch(self, *, now: float | None = None) -> WindowSnapshot:
44
+ snapshot = self._resolve_snapshot()
45
+ self._prefetched = (self._clock(now), snapshot)
46
+ return snapshot
47
+
48
+ def capture(self, *, now: float | None = None) -> Any:
49
+ return self.output_backend.capture(self._target_snapshot(now=now))
50
+
51
+ def observe(self, *, now: float | None = None) -> WindowSnapshot:
52
+ observe = getattr(self.output_backend, "observe", None)
53
+ target = self._target_snapshot(now=now)
54
+ if observe is None:
55
+ return target
56
+ return observe(target)
57
+
58
+ def click(self, x: int | float, y: int | float, *, now: float | None = None) -> OperationResult:
59
+ target = self._target_snapshot(now=now)
60
+ px, py = target.normalize_point(x, y)
61
+ return self._perform(target, Operation(kind="click", x=px, y=py))
62
+
63
+ def key_down(self, key: str, *, now: float | None = None) -> OperationResult:
64
+ return self._perform(
65
+ self._target_snapshot(now=now),
66
+ Operation(kind="key_down", key=key),
67
+ )
68
+
69
+ def key_up(self, key: str, *, now: float | None = None) -> OperationResult:
70
+ return self._perform(
71
+ self._target_snapshot(now=now),
72
+ Operation(kind="key_up", key=key),
73
+ )
74
+
75
+ def type_text(self, text: str, *, now: float | None = None) -> OperationResult:
76
+ return self._perform(
77
+ self._target_snapshot(now=now),
78
+ Operation(kind="type_text", text=text),
79
+ )
80
+
81
+ def _perform(self, target: WindowSnapshot, operation: Operation) -> OperationResult:
82
+ return self.input_backend.perform(target, operation)
83
+
84
+ def _target_snapshot(self, *, now: float | None = None) -> WindowSnapshot:
85
+ current = self._clock(now)
86
+ if self._prefetched is not None:
87
+ resolved_at, snapshot = self._prefetched
88
+ fresh = current - resolved_at <= self.config.prefetch_ttl_seconds
89
+ if fresh and self._snapshot_is_valid(snapshot):
90
+ return snapshot
91
+ return self.prefetch(now=current)
92
+
93
+ def _resolve_snapshot(self) -> WindowSnapshot:
94
+ snapshot = self.resolver.resolve(self.selector)
95
+ if snapshot is None:
96
+ raise WindowNotFoundError(f"window not found: {self.selector}")
97
+ return snapshot
98
+
99
+ def _snapshot_is_valid(self, snapshot: WindowSnapshot) -> bool:
100
+ validator = getattr(self.resolver, "is_valid", None)
101
+ if validator is None:
102
+ return snapshot.visible
103
+ return bool(validator(snapshot))
104
+
105
+ def _validate_backends(self) -> None:
106
+ input_mode = getattr(self.input_backend, "mode", None)
107
+ allowed_input_modes = {
108
+ self.profile.input_mode,
109
+ *self.config.allowed_input_fallbacks,
110
+ }
111
+ if input_mode not in allowed_input_modes:
112
+ raise BackendCompatibilityError(
113
+ "input backend mode "
114
+ f"{input_mode!r} is not allowed for profile {self.profile.name!r}"
115
+ )
116
+
117
+ output_mode = getattr(self.output_backend, "mode", None)
118
+ if output_mode != self.profile.output_mode:
119
+ raise BackendCompatibilityError(
120
+ "output backend mode "
121
+ f"{output_mode!r} does not match profile {self.profile.name!r}"
122
+ )
123
+
124
+ if self.profile.requires_hook:
125
+ capabilities = getattr(self.input_backend, "capabilities", frozenset())
126
+ if Capability.HOOKED_INPUT not in capabilities:
127
+ raise BackendCompatibilityError(
128
+ f"profile {self.profile.name!r} requires a hook-capable input backend"
129
+ )
130
+
131
+ @staticmethod
132
+ def _clock(now: float | None) -> float:
133
+ return time.monotonic() if now is None else now
134
+
135
+
136
+ def open_window(
137
+ *,
138
+ selector: WindowSelector,
139
+ resolver: WindowResolver,
140
+ input_backend: InputBackend,
141
+ output_backend: OutputBackend,
142
+ config: AutomationConfig | None = None,
143
+ ) -> WindowSession:
144
+ return WindowSession(
145
+ selector=selector,
146
+ resolver=resolver,
147
+ input_backend=input_backend,
148
+ output_backend=output_backend,
149
+ config=config,
150
+ )