gridfleet-testkit 0.8.0__tar.gz → 0.9.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/CHANGELOG.md +21 -0
  2. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/PKG-INFO +39 -6
  3. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/README.md +37 -3
  4. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/_example_helpers.py +10 -5
  5. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_android_mobile_screenshot.py +3 -4
  6. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_android_tv_screenshot.py +3 -4
  7. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_firetv_screenshot.py +3 -4
  8. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_ios_simulator_screenshot.py +3 -4
  9. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_roku_screenshot.py +3 -4
  10. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_roku_sideload_screenshot.py +3 -4
  11. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/test_tvos_screenshot.py +3 -4
  12. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/__init__.py +1 -1
  13. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/allocation.py +25 -15
  14. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/appium.py +34 -34
  15. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/client.py +68 -54
  16. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/pytest_plugin.py +31 -14
  17. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/sessions.py +25 -8
  18. gridfleet_testkit-0.9.1/gridfleet_testkit/types.py +11 -0
  19. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/pyproject.toml +3 -11
  20. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_allocation.py +21 -7
  21. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_appium.py +9 -19
  22. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_client.py +125 -97
  23. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_client_test_data.py +12 -9
  24. gridfleet_testkit-0.9.1/tests/test_driver_agnostic_guard.py +57 -0
  25. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_pytest_plugin.py +33 -28
  26. gridfleet_testkit-0.9.1/tests/test_pytest_plugin_grid_run_id_injection.py +29 -0
  27. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_pytest_plugin_test_data.py +2 -2
  28. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_sessions.py +5 -2
  29. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/uv.lock +4 -6
  30. gridfleet_testkit-0.8.0/tests/test_driver_agnostic_guard.py +0 -29
  31. gridfleet_testkit-0.8.0/tests/test_pytest_plugin_grid_run_id_injection.py +0 -56
  32. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/.gitignore +0 -0
  33. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/__init__.py +0 -0
  34. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/examples/assets/hello-world.zip +0 -0
  35. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/gridfleet_testkit/py.typed +0 -0
  36. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_docs_contract.py +0 -0
  37. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_package_metadata.py +0 -0
  38. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.1}/tests/test_sessions_resolve_device_handle.py +0 -0
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
4
4
 
5
+ ## [0.9.1](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.9.0...gridfleet-testkit-v0.9.1) (2026-05-13)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **testkit:** align release policy with commitlint ([5dd8220](https://github.com/quidow/gridfleet/commit/5dd822010460e994f2e3c5b6676a69bed05678ed))
11
+
12
+ ## [0.9.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.8.0...gridfleet-testkit-v0.9.0) (2026-05-12)
13
+
14
+
15
+ ### Features
16
+
17
+ * **testkit:** add run detail client helper ([c80a8cc](https://github.com/quidow/gridfleet/commit/c80a8cc95093c2f46a7e714c96ff0b33018af5ba))
18
+ * **testkit:** expose allocation device tags ([0bf5e2e](https://github.com/quidow/gridfleet/commit/0bf5e2e04b806fadd0afcd3c95073828d0c2414e))
19
+ * **testkit:** support tag-based device targeting ([db0d0e3](https://github.com/quidow/gridfleet/commit/db0d0e3d3d1231828bb22a707d3bdcab6c0ec717))
20
+
21
+
22
+ ### Documentation
23
+
24
+ * **testkit:** document tag-based device targeting ([096841b](https://github.com/quidow/gridfleet/commit/096841b737dec71524d0edfa4c538d9cc69e7c2c))
25
+
5
26
  ## [0.8.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.7.0...gridfleet-testkit-v0.8.0) (2026-05-12)
6
27
 
7
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridfleet-testkit
3
- Version: 0.8.0
3
+ Version: 0.9.1
4
4
  Summary: Supported pytest and run-orchestration helpers for GridFleet integrations
5
5
  Project-URL: Homepage, https://github.com/quidow/gridfleet
6
6
  Project-URL: Repository, https://github.com/quidow/gridfleet
@@ -22,10 +22,9 @@ Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python :: 3.14
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: <3.15,>=3.10
25
+ Requires-Dist: appium-python-client<6,>=4.5
25
26
  Requires-Dist: httpx<1,>=0.27
26
27
  Requires-Dist: pytest<10,>=9.0.3
27
- Provides-Extra: appium
28
- Requires-Dist: appium-python-client<6,>=4.5; extra == 'appium'
29
28
  Provides-Extra: dev
30
29
  Requires-Dist: mypy<3,>=1.20.2; extra == 'dev'
31
30
  Requires-Dist: pytest<10,>=9.0.3; extra == 'dev'
@@ -81,19 +80,19 @@ The supported contract is the installable package and documented import pattern.
81
80
  From PyPI:
82
81
 
83
82
  ```bash
84
- pip install "gridfleet-testkit[appium]"
83
+ pip install "gridfleet-testkit"
85
84
  ```
86
85
 
87
86
  From a local checkout:
88
87
 
89
88
  ```bash
90
- uv pip install -e ./testkit[appium]
89
+ uv pip install -e ./testkit
91
90
  ```
92
91
 
93
92
  From a copied `testkit/` directory inside another repository:
94
93
 
95
94
  ```bash
96
- uv pip install -e ./testkit[appium]
95
+ uv pip install -e ./testkit
97
96
  ```
98
97
 
99
98
  From a Git checkout or VCS URL that contains this package:
@@ -103,6 +102,7 @@ uv pip install "git+https://github.com/<org>/<repo>.git#subdirectory=testkit"
103
102
  ```
104
103
 
105
104
  The package supports Python 3.10 and newer.
105
+ `Appium-Python-Client` is installed as a runtime dependency because the pytest fixtures create real Appium sessions.
106
106
 
107
107
  ## Environment
108
108
 
@@ -192,6 +192,7 @@ finally:
192
192
  | `GridFleetClient.get_device_by_connection_target(connection_target)` | Fetch one device detail row by runtime connection target |
193
193
  | `GridFleetClient.get_device_capabilities(device_id)` | Fetch current Appium capability metadata for a device |
194
194
  | `GridFleetClient.get_device_test_data(device_id)` | Fetch operator-attached free-form test_data for a device |
195
+ | `GridFleetClient.get_run(run_id)` | Fetch one run detail row by backend run id |
195
196
  | `GridFleetClient.replace_device_test_data(device_id, body)` | Replace test_data with the supplied object |
196
197
  | `GridFleetClient.merge_device_test_data(device_id, body)` | Deep-merge into device test_data |
197
198
  | `GridFleetClient.resolve_device_id_by_connection_target(connection_target)` | Resolve the backend device id for a runtime connection target |
@@ -216,6 +217,38 @@ finally:
216
217
  | `get_device_test_data_for_driver(driver, gridfleet_client=None)` | Fetch test_data for a live Appium driver |
217
218
  | `register_run_cleanup(client, run_id, heartbeat_thread=None)` | Register `atexit` cleanup callable and return it; stops the heartbeat thread on exit but does not complete or cancel the run by default |
218
219
 
220
+ ### Targeting Devices by Tag
221
+
222
+ GridFleet injects device tags into Grid node stereotypes as `appium:gridfleet:tag:<key>` capabilities, so Selenium Grid can route sessions to devices matching specific tags.
223
+
224
+ ```python
225
+ @pytest.mark.parametrize(
226
+ "appium_driver",
227
+ [
228
+ {
229
+ "pack_id": "appium-uiautomator2",
230
+ "platform_id": "android_mobile",
231
+ "appium:gridfleet:tag:screen_type": "4k",
232
+ }
233
+ ],
234
+ indirect=True,
235
+ )
236
+ def test_4k_display(appium_driver):
237
+ ...
238
+ ```
239
+
240
+ The same capability works for free sessions:
241
+
242
+ ```python
243
+ driver = create_appium_driver(
244
+ pack_id="appium-uiautomator2",
245
+ platform_id="android_mobile",
246
+ capabilities={"appium:gridfleet:tag:screen_type": "4k"},
247
+ )
248
+ ```
249
+
250
+ When an operator edits device tags, GridFleet marks the device for re-verification. The next verification restarts the Appium node and re-registers it with the updated Grid stereotype.
251
+
219
252
  ### Worker Identity
220
253
 
221
254
  `worker_id` is an arbitrary string used for reservation telemetry and run attribution. For pytest-xdist, pass `request.config.workerinput["workerid"]` from worker processes; values are normally `gw0`, `gw1`, and so on. For controller-only flows, use `"controller"` or a stable hostname. For custom schedulers, use a UUID or job-specific worker name.
@@ -47,19 +47,19 @@ The supported contract is the installable package and documented import pattern.
47
47
  From PyPI:
48
48
 
49
49
  ```bash
50
- pip install "gridfleet-testkit[appium]"
50
+ pip install "gridfleet-testkit"
51
51
  ```
52
52
 
53
53
  From a local checkout:
54
54
 
55
55
  ```bash
56
- uv pip install -e ./testkit[appium]
56
+ uv pip install -e ./testkit
57
57
  ```
58
58
 
59
59
  From a copied `testkit/` directory inside another repository:
60
60
 
61
61
  ```bash
62
- uv pip install -e ./testkit[appium]
62
+ uv pip install -e ./testkit
63
63
  ```
64
64
 
65
65
  From a Git checkout or VCS URL that contains this package:
@@ -69,6 +69,7 @@ uv pip install "git+https://github.com/<org>/<repo>.git#subdirectory=testkit"
69
69
  ```
70
70
 
71
71
  The package supports Python 3.10 and newer.
72
+ `Appium-Python-Client` is installed as a runtime dependency because the pytest fixtures create real Appium sessions.
72
73
 
73
74
  ## Environment
74
75
 
@@ -158,6 +159,7 @@ finally:
158
159
  | `GridFleetClient.get_device_by_connection_target(connection_target)` | Fetch one device detail row by runtime connection target |
159
160
  | `GridFleetClient.get_device_capabilities(device_id)` | Fetch current Appium capability metadata for a device |
160
161
  | `GridFleetClient.get_device_test_data(device_id)` | Fetch operator-attached free-form test_data for a device |
162
+ | `GridFleetClient.get_run(run_id)` | Fetch one run detail row by backend run id |
161
163
  | `GridFleetClient.replace_device_test_data(device_id, body)` | Replace test_data with the supplied object |
162
164
  | `GridFleetClient.merge_device_test_data(device_id, body)` | Deep-merge into device test_data |
163
165
  | `GridFleetClient.resolve_device_id_by_connection_target(connection_target)` | Resolve the backend device id for a runtime connection target |
@@ -182,6 +184,38 @@ finally:
182
184
  | `get_device_test_data_for_driver(driver, gridfleet_client=None)` | Fetch test_data for a live Appium driver |
183
185
  | `register_run_cleanup(client, run_id, heartbeat_thread=None)` | Register `atexit` cleanup callable and return it; stops the heartbeat thread on exit but does not complete or cancel the run by default |
184
186
 
187
+ ### Targeting Devices by Tag
188
+
189
+ GridFleet injects device tags into Grid node stereotypes as `appium:gridfleet:tag:<key>` capabilities, so Selenium Grid can route sessions to devices matching specific tags.
190
+
191
+ ```python
192
+ @pytest.mark.parametrize(
193
+ "appium_driver",
194
+ [
195
+ {
196
+ "pack_id": "appium-uiautomator2",
197
+ "platform_id": "android_mobile",
198
+ "appium:gridfleet:tag:screen_type": "4k",
199
+ }
200
+ ],
201
+ indirect=True,
202
+ )
203
+ def test_4k_display(appium_driver):
204
+ ...
205
+ ```
206
+
207
+ The same capability works for free sessions:
208
+
209
+ ```python
210
+ driver = create_appium_driver(
211
+ pack_id="appium-uiautomator2",
212
+ platform_id="android_mobile",
213
+ capabilities={"appium:gridfleet:tag:screen_type": "4k"},
214
+ )
215
+ ```
216
+
217
+ When an operator edits device tags, GridFleet marks the device for re-verification. The next verification restarts the Appium node and re-registers it with the updated Grid stereotype.
218
+
185
219
  ### Worker Identity
186
220
 
187
221
  `worker_id` is an arbitrary string used for reservation telemetry and run attribution. For pytest-xdist, pass `request.config.workerinput["workerid"]` from worker processes; values are normally `gw0`, `gw1`, and so on. For controller-only flows, use `"controller"` or a stable hostname. For custom schedulers, use a UUID or job-specific worker name.
@@ -3,13 +3,18 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
- from typing import Any
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Mapping
10
+
11
+ from appium.webdriver.webdriver import WebDriver
7
12
 
8
13
  SCREENSHOT_DIR = Path(__file__).parent / "screenshots"
9
14
  ROKU_HELLO_WORLD_APP = Path(__file__).parent / "assets" / "hello-world.zip"
10
15
 
11
16
 
12
- def _resolved_connection_target(capabilities: dict[str, Any]) -> str:
17
+ def _resolved_connection_target(capabilities: Mapping[str, object]) -> str:
13
18
  value = capabilities.get("appium:udid")
14
19
  if isinstance(value, str) and value:
15
20
  return value
@@ -19,7 +24,7 @@ def _resolved_connection_target(capabilities: dict[str, Any]) -> str:
19
24
  return "session"
20
25
 
21
26
 
22
- def print_connection_context(driver: Any) -> str:
27
+ def print_connection_context(driver: WebDriver) -> str:
23
28
  """Print the resolved session context and return the connection target string."""
24
29
  caps = driver.capabilities
25
30
  connection_target = _resolved_connection_target(caps)
@@ -35,7 +40,7 @@ def print_connection_context(driver: Any) -> str:
35
40
  return connection_target
36
41
 
37
42
 
38
- def save_and_assert_screenshot(driver: Any, example_name: str) -> Path:
43
+ def save_and_assert_screenshot(driver: WebDriver, example_name: str) -> Path:
39
44
  """Save a screenshot and assert that the written file is non-empty."""
40
45
  caps = driver.capabilities
41
46
  connection_target = _resolved_connection_target(caps).replace(":", "_")
@@ -51,7 +56,7 @@ def save_and_assert_screenshot(driver: Any, example_name: str) -> Path:
51
56
  return screenshot_path
52
57
 
53
58
 
54
- def install_and_activate_roku_dev_app(driver: Any) -> None:
59
+ def install_and_activate_roku_dev_app(driver: WebDriver) -> None:
55
60
  """Install and activate the bundled Roku dev app used by screenshot examples."""
56
61
  assert ROKU_HELLO_WORLD_APP.exists(), f"App package not found: {ROKU_HELLO_WORLD_APP}"
57
62
  driver.install_app(str(ROKU_HELLO_WORLD_APP.resolve()))
@@ -5,15 +5,14 @@ Requires:
5
5
  - Selenium Grid hub running on localhost:4444
6
6
  - An Android mobile device registered and its Appium node running
7
7
  - The supported GridFleet testkit installed
8
- - Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
8
+ - Appium-Python-Client installed (`uv pip install -e ./testkit`)
9
9
 
10
10
  Run:
11
11
  cd testkit && python -m pytest examples/test_android_mobile_screenshot.py -v -s
12
12
  """
13
13
 
14
- from typing import Any
15
-
16
14
  import pytest
15
+ from appium.webdriver.webdriver import WebDriver
17
16
 
18
17
  from examples._example_helpers import print_connection_context, save_and_assert_screenshot
19
18
 
@@ -30,7 +29,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
30
29
  ],
31
30
  indirect=True,
32
31
  )
33
- def test_android_mobile_take_screenshot(appium_driver: Any) -> None:
32
+ def test_android_mobile_take_screenshot(appium_driver: WebDriver) -> None:
34
33
  """Connect to an Android mobile device through the Grid and take a screenshot."""
35
34
  driver = appium_driver
36
35
 
@@ -5,15 +5,14 @@ Requires:
5
5
  - Selenium Grid hub running on localhost:4444
6
6
  - An Android TV device registered and its Appium node running
7
7
  - The supported GridFleet testkit installed
8
- - Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
8
+ - Appium-Python-Client installed (`uv pip install -e ./testkit`)
9
9
 
10
10
  Run:
11
11
  cd testkit && python -m pytest examples/test_android_tv_screenshot.py -v -s
12
12
  """
13
13
 
14
- from typing import Any
15
-
16
14
  import pytest
15
+ from appium.webdriver.webdriver import WebDriver
17
16
 
18
17
  from examples._example_helpers import print_connection_context, save_and_assert_screenshot
19
18
 
@@ -30,7 +29,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
30
29
  ],
31
30
  indirect=True,
32
31
  )
33
- def test_android_tv_take_screenshot(appium_driver: Any) -> None:
32
+ def test_android_tv_take_screenshot(appium_driver: WebDriver) -> None:
34
33
  """Connect to an Android TV device through the Grid and take a screenshot."""
35
34
  driver = appium_driver
36
35
 
@@ -5,15 +5,14 @@ Requires:
5
5
  - Selenium Grid hub running on localhost:4444
6
6
  - A Fire TV device registered and its Appium node running
7
7
  - The supported GridFleet testkit installed
8
- - Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
8
+ - Appium-Python-Client installed (`uv pip install -e ./testkit`)
9
9
 
10
10
  Run:
11
11
  cd testkit && python -m pytest examples/test_firetv_screenshot.py -v -s
12
12
  """
13
13
 
14
- from typing import Any
15
-
16
14
  import pytest
15
+ from appium.webdriver.webdriver import WebDriver
17
16
 
18
17
  from examples._example_helpers import print_connection_context, save_and_assert_screenshot
19
18
 
@@ -32,7 +31,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
32
31
  ],
33
32
  indirect=True,
34
33
  )
35
- def test_firetv_take_screenshot(appium_driver: Any) -> None:
34
+ def test_firetv_take_screenshot(appium_driver: WebDriver) -> None:
36
35
  """Connect to a Fire TV device through the Grid and take a screenshot."""
37
36
  driver = appium_driver
38
37
 
@@ -6,15 +6,14 @@ Requires:
6
6
  - An iOS simulator registered and its Appium node running
7
7
  - The supported GridFleet testkit installed
8
8
  - Appium with the XCUITest driver installed (`appium driver install xcuitest`)
9
- - Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
9
+ - Appium-Python-Client installed (`uv pip install -e ./testkit`)
10
10
 
11
11
  Run:
12
12
  cd testkit && python -m pytest examples/test_ios_simulator_screenshot.py -v -s
13
13
  """
14
14
 
15
- from typing import Any
16
-
17
15
  import pytest
16
+ from appium.webdriver.webdriver import WebDriver
18
17
 
19
18
  from examples._example_helpers import print_connection_context, save_and_assert_screenshot
20
19
 
@@ -32,7 +31,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
32
31
  ],
33
32
  indirect=True,
34
33
  )
35
- def test_ios_take_screenshot(appium_driver: Any) -> None:
34
+ def test_ios_take_screenshot(appium_driver: WebDriver) -> None:
36
35
  """Connect to an iOS simulator through the Grid and take a screenshot."""
37
36
  driver = appium_driver
38
37
 
@@ -6,15 +6,14 @@ Requires:
6
6
  - A Roku device registered with Roku dev credentials in device config
7
7
  - Appium with the Roku driver installed (`appium driver install roku`)
8
8
  - The supported GridFleet testkit installed
9
- - Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
9
+ - Appium-Python-Client installed (`uv pip install -e ./testkit`)
10
10
 
11
11
  Run:
12
12
  cd testkit && python -m pytest examples/test_roku_screenshot.py -v -s
13
13
  """
14
14
 
15
- from typing import Any
16
-
17
15
  import pytest
16
+ from appium.webdriver.webdriver import WebDriver
18
17
 
19
18
  from examples._example_helpers import (
20
19
  install_and_activate_roku_dev_app,
@@ -35,7 +34,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
35
34
  ],
36
35
  indirect=True,
37
36
  )
38
- def test_roku_take_screenshot(appium_driver: Any) -> None:
37
+ def test_roku_take_screenshot(appium_driver: WebDriver) -> None:
39
38
  """Connect to a Roku device through the Grid and take a screenshot."""
40
39
  driver = appium_driver
41
40
 
@@ -6,15 +6,14 @@ Requires:
6
6
  - A Roku device registered with Roku dev credentials in device config
7
7
  - Appium with the Roku driver installed (`appium driver install roku`)
8
8
  - The supported GridFleet testkit installed
9
- - Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
9
+ - Appium-Python-Client installed (`uv pip install -e ./testkit`)
10
10
 
11
11
  Run:
12
12
  cd testkit && python -m pytest examples/test_roku_sideload_screenshot.py -v -s
13
13
  """
14
14
 
15
- from typing import Any
16
-
17
15
  import pytest
16
+ from appium.webdriver.webdriver import WebDriver
18
17
 
19
18
  from examples._example_helpers import (
20
19
  ROKU_HELLO_WORLD_APP,
@@ -36,7 +35,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
36
35
  ],
37
36
  indirect=True,
38
37
  )
39
- def test_roku_install_app_and_take_screenshot(appium_driver: Any) -> None:
38
+ def test_roku_install_app_and_take_screenshot(appium_driver: WebDriver) -> None:
40
39
  """Connect to a Roku device, sideload an app, and take a screenshot."""
41
40
  driver = appium_driver
42
41
 
@@ -7,15 +7,14 @@ Requires:
7
7
  - The supported GridFleet testkit installed
8
8
  - Appium with the XCUITest driver installed (`appium driver install xcuitest`)
9
9
  - tvOS real-device prerequisites already configured on the host, including WebDriverAgent/XCUITest setup
10
- - Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
10
+ - Appium-Python-Client installed (`uv pip install -e ./testkit`)
11
11
 
12
12
  Run:
13
13
  cd testkit && python -m pytest examples/test_tvos_screenshot.py -v -s
14
14
  """
15
15
 
16
- from typing import Any
17
-
18
16
  import pytest
17
+ from appium.webdriver.webdriver import WebDriver
19
18
 
20
19
  from examples._example_helpers import print_connection_context, save_and_assert_screenshot
21
20
 
@@ -32,7 +31,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
32
31
  ],
33
32
  indirect=True,
34
33
  )
35
- def test_tvos_take_screenshot(appium_driver: Any) -> None:
34
+ def test_tvos_take_screenshot(appium_driver: WebDriver) -> None:
36
35
  """Connect to a tvOS real device through the Grid and take a screenshot."""
37
36
  driver = appium_driver
38
37
 
@@ -47,7 +47,7 @@ from .sessions import build_error_session_payload, resolve_device_handle_from_dr
47
47
  try:
48
48
  __version__ = version("gridfleet-testkit")
49
49
  except PackageNotFoundError:
50
- __version__ = "0.8.0"
50
+ __version__ = "0.9.1"
51
51
 
52
52
  __all__ = [
53
53
  "GRIDFLEET_API_URL",
@@ -3,10 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass, replace
6
- from typing import TYPE_CHECKING, Any
6
+ from typing import TYPE_CHECKING, cast
7
7
 
8
8
  if TYPE_CHECKING:
9
+ from appium.webdriver.webdriver import WebDriver
10
+
9
11
  from .client import GridFleetClient
12
+ from .types import JsonObject
10
13
 
11
14
 
12
15
  @dataclass(frozen=True)
@@ -35,10 +38,11 @@ class AllocatedDevice:
35
38
  connection_type: str
36
39
  manufacturer: str | None
37
40
  model: str | None
38
- config: dict[str, Any] | None
39
- live_capabilities: dict[str, Any] | None
40
- test_data: dict[str, Any] | None = None
41
+ config: JsonObject | None
42
+ live_capabilities: JsonObject | None
43
+ test_data: JsonObject | None = None
41
44
  unavailable_includes: tuple[UnavailableInclude, ...] = ()
45
+ tags: dict[str, str] | None = None
42
46
 
43
47
  @property
44
48
  def is_real_device(self) -> bool:
@@ -71,23 +75,23 @@ class AllocatedDevice:
71
75
  return self.platform_label or self.platform_id
72
76
 
73
77
 
74
- def _string_value(payload: dict[str, Any], key: str, *, default: str | None = None) -> str:
78
+ def _string_value(payload: JsonObject, key: str, *, default: str | None = None) -> str:
75
79
  value = payload.get(key, default)
76
80
  if isinstance(value, str) and value:
77
81
  return value
78
82
  raise ValueError(f"Allocated device payload is missing {key}")
79
83
 
80
84
 
81
- def _optional_string_value(payload: dict[str, Any], key: str) -> str | None:
85
+ def _optional_string_value(payload: JsonObject, key: str) -> str | None:
82
86
  value = payload.get(key)
83
87
  return value if isinstance(value, str) and value else None
84
88
 
85
89
 
86
- def _needs_device_detail(payload: dict[str, Any]) -> bool:
90
+ def _needs_device_detail(payload: JsonObject) -> bool:
87
91
  return any(payload.get(key) is None for key in ("name", "device_type", "connection_type", "manufacturer", "model"))
88
92
 
89
93
 
90
- def _merge_device_detail(payload: dict[str, Any], detail: dict[str, Any]) -> dict[str, Any]:
94
+ def _merge_device_detail(payload: JsonObject, detail: JsonObject) -> JsonObject:
91
95
  merged = dict(payload)
92
96
  for key in ("name", "device_type", "connection_type", "manufacturer", "model"):
93
97
  if merged.get(key) is None and detail.get(key) is not None:
@@ -97,7 +101,7 @@ def _merge_device_detail(payload: dict[str, Any], detail: dict[str, Any]) -> dic
97
101
  return merged
98
102
 
99
103
 
100
- def _parse_unavailable_includes(payload: dict[str, Any]) -> tuple[UnavailableInclude, ...]:
104
+ def _parse_unavailable_includes(payload: JsonObject) -> tuple[UnavailableInclude, ...]:
101
105
  raw = payload.get("unavailable_includes")
102
106
  if not isinstance(raw, list):
103
107
  return ()
@@ -113,7 +117,7 @@ def _parse_unavailable_includes(payload: dict[str, Any]) -> tuple[UnavailableInc
113
117
 
114
118
 
115
119
  def hydrate_allocated_device(
116
- device_handle: dict[str, Any],
120
+ device_handle: JsonObject,
117
121
  *,
118
122
  run_id: str,
119
123
  client: GridFleetClient,
@@ -133,14 +137,14 @@ def hydrate_allocated_device(
133
137
  connection_target = _optional_string_value(payload, "connection_target")
134
138
  inline_config = payload.get("config")
135
139
  if isinstance(inline_config, dict):
136
- config: dict[str, Any] | None = inline_config
140
+ config: JsonObject | None = cast("JsonObject", inline_config)
137
141
  elif fetch_config and connection_target and "config" not in unavailable_set:
138
142
  config = client.get_device_config(connection_target)
139
143
  else:
140
144
  config = None
141
145
  inline_capabilities = payload.get("live_capabilities")
142
146
  if isinstance(inline_capabilities, dict):
143
- live_capabilities: dict[str, Any] | None = inline_capabilities
147
+ live_capabilities: JsonObject | None = cast("JsonObject", inline_capabilities)
144
148
  elif fetch_capabilities and "capabilities" not in unavailable_set:
145
149
  live_capabilities = client.get_device_capabilities(device_id)
146
150
  else:
@@ -148,11 +152,16 @@ def hydrate_allocated_device(
148
152
 
149
153
  inline_test_data = payload.get("test_data")
150
154
  if isinstance(inline_test_data, dict):
151
- test_data: dict[str, Any] | None = inline_test_data
155
+ test_data: JsonObject | None = cast("JsonObject", inline_test_data)
152
156
  elif fetch_test_data and "test_data" not in unavailable_set:
153
157
  test_data = client.get_device_test_data(device_id)
154
158
  else:
155
159
  test_data = None
160
+ inline_tags = payload.get("tags")
161
+ if isinstance(inline_tags, dict):
162
+ tags: dict[str, str] | None = inline_tags
163
+ else:
164
+ tags = None
156
165
 
157
166
  return AllocatedDevice(
158
167
  run_id=run_id,
@@ -173,19 +182,20 @@ def hydrate_allocated_device(
173
182
  live_capabilities=live_capabilities,
174
183
  test_data=test_data,
175
184
  unavailable_includes=unavailable_includes,
185
+ tags=tags,
176
186
  )
177
187
 
178
188
 
179
189
  def hydrate_allocated_device_from_driver(
180
190
  allocated: AllocatedDevice,
181
- driver: Any,
191
+ driver: WebDriver,
182
192
  *,
183
193
  client: GridFleetClient,
184
194
  ) -> AllocatedDevice:
185
195
  """Refresh live capabilities from a running Appium driver session."""
186
196
  capabilities = getattr(driver, "capabilities", None)
187
197
  if isinstance(capabilities, dict):
188
- live_capabilities = dict(capabilities)
198
+ live_capabilities = cast("JsonObject", dict(capabilities))
189
199
  else:
190
200
  live_capabilities = client.get_device_capabilities(allocated.device_id)
191
201
  return replace(allocated, live_capabilities=live_capabilities)