gridfleet-testkit 0.9.0__tar.gz → 0.9.2__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.9.0 → gridfleet_testkit-0.9.2}/CHANGELOG.md +14 -0
  2. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/PKG-INFO +6 -6
  3. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/README.md +4 -3
  4. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/_example_helpers.py +10 -5
  5. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_android_mobile_screenshot.py +3 -6
  6. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_android_tv_screenshot.py +3 -6
  7. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_firetv_screenshot.py +3 -6
  8. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_ios_simulator_screenshot.py +3 -6
  9. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_roku_screenshot.py +3 -6
  10. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_roku_sideload_screenshot.py +3 -6
  11. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/test_tvos_screenshot.py +3 -6
  12. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/__init__.py +1 -1
  13. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/allocation.py +18 -15
  14. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/appium.py +34 -34
  15. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/client.py +60 -56
  16. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/pytest_plugin.py +31 -14
  17. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/sessions.py +25 -8
  18. gridfleet_testkit-0.9.2/gridfleet_testkit/types.py +11 -0
  19. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/pyproject.toml +3 -11
  20. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_allocation.py +10 -7
  21. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_appium.py +9 -19
  22. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_client.py +105 -98
  23. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_client_test_data.py +12 -9
  24. gridfleet_testkit-0.9.2/tests/test_driver_agnostic_guard.py +57 -0
  25. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_pytest_plugin.py +6 -28
  26. gridfleet_testkit-0.9.2/tests/test_pytest_plugin_grid_run_id_injection.py +29 -0
  27. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_pytest_plugin_test_data.py +2 -2
  28. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_sessions.py +5 -2
  29. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/uv.lock +4 -6
  30. gridfleet_testkit-0.9.0/tests/test_driver_agnostic_guard.py +0 -29
  31. gridfleet_testkit-0.9.0/tests/test_pytest_plugin_grid_run_id_injection.py +0 -56
  32. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/.gitignore +0 -0
  33. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/__init__.py +0 -0
  34. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/examples/assets/hello-world.zip +0 -0
  35. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/gridfleet_testkit/py.typed +0 -0
  36. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_docs_contract.py +0 -0
  37. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_package_metadata.py +0 -0
  38. {gridfleet_testkit-0.9.0 → gridfleet_testkit-0.9.2}/tests/test_sessions_resolve_device_handle.py +0 -0
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
4
4
 
5
+ ## [0.9.2](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.9.1...gridfleet-testkit-v0.9.2) (2026-05-16)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **testkit:** remove pytest_plugins shim and stale --extra appium docs ([#270](https://github.com/quidow/gridfleet/issues/270)) ([1165fc1](https://github.com/quidow/gridfleet/commit/1165fc1e7326853e7467500c41d8b1c197e7046f))
11
+
12
+ ## [0.9.1](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.9.0...gridfleet-testkit-v0.9.1) (2026-05-13)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **testkit:** align release policy with commitlint ([5dd8220](https://github.com/quidow/gridfleet/commit/5dd822010460e994f2e3c5b6676a69bed05678ed))
18
+
5
19
  ## [0.9.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.8.0...gridfleet-testkit-v0.9.0) (2026-05-12)
6
20
 
7
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridfleet-testkit
3
- Version: 0.9.0
3
+ Version: 0.9.2
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
 
@@ -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
 
@@ -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,20 +5,17 @@ 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
 
20
- pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
21
-
22
19
 
23
20
  @pytest.mark.parametrize(
24
21
  "appium_driver",
@@ -30,7 +27,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
30
27
  ],
31
28
  indirect=True,
32
29
  )
33
- def test_android_mobile_take_screenshot(appium_driver: Any) -> None:
30
+ def test_android_mobile_take_screenshot(appium_driver: WebDriver) -> None:
34
31
  """Connect to an Android mobile device through the Grid and take a screenshot."""
35
32
  driver = appium_driver
36
33
 
@@ -5,20 +5,17 @@ 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
 
20
- pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
21
-
22
19
 
23
20
  @pytest.mark.parametrize(
24
21
  "appium_driver",
@@ -30,7 +27,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
30
27
  ],
31
28
  indirect=True,
32
29
  )
33
- def test_android_tv_take_screenshot(appium_driver: Any) -> None:
30
+ def test_android_tv_take_screenshot(appium_driver: WebDriver) -> None:
34
31
  """Connect to an Android TV device through the Grid and take a screenshot."""
35
32
  driver = appium_driver
36
33
 
@@ -5,20 +5,17 @@ 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
 
20
- pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
21
-
22
19
 
23
20
  @pytest.mark.parametrize(
24
21
  "appium_driver",
@@ -32,7 +29,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
32
29
  ],
33
30
  indirect=True,
34
31
  )
35
- def test_firetv_take_screenshot(appium_driver: Any) -> None:
32
+ def test_firetv_take_screenshot(appium_driver: WebDriver) -> None:
36
33
  """Connect to a Fire TV device through the Grid and take a screenshot."""
37
34
  driver = appium_driver
38
35
 
@@ -6,20 +6,17 @@ 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
 
21
- pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
22
-
23
20
 
24
21
  @pytest.mark.parametrize(
25
22
  "appium_driver",
@@ -32,7 +29,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
32
29
  ],
33
30
  indirect=True,
34
31
  )
35
- def test_ios_take_screenshot(appium_driver: Any) -> None:
32
+ def test_ios_take_screenshot(appium_driver: WebDriver) -> None:
36
33
  """Connect to an iOS simulator through the Grid and take a screenshot."""
37
34
  driver = appium_driver
38
35
 
@@ -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,
@@ -22,8 +21,6 @@ from examples._example_helpers import (
22
21
  save_and_assert_screenshot,
23
22
  )
24
23
 
25
- pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
26
-
27
24
 
28
25
  @pytest.mark.parametrize(
29
26
  "appium_driver",
@@ -35,7 +32,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
35
32
  ],
36
33
  indirect=True,
37
34
  )
38
- def test_roku_take_screenshot(appium_driver: Any) -> None:
35
+ def test_roku_take_screenshot(appium_driver: WebDriver) -> None:
39
36
  """Connect to a Roku device through the Grid and take a screenshot."""
40
37
  driver = appium_driver
41
38
 
@@ -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,
@@ -23,8 +22,6 @@ from examples._example_helpers import (
23
22
  save_and_assert_screenshot,
24
23
  )
25
24
 
26
- pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
27
-
28
25
 
29
26
  @pytest.mark.parametrize(
30
27
  "appium_driver",
@@ -36,7 +33,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
36
33
  ],
37
34
  indirect=True,
38
35
  )
39
- def test_roku_install_app_and_take_screenshot(appium_driver: Any) -> None:
36
+ def test_roku_install_app_and_take_screenshot(appium_driver: WebDriver) -> None:
40
37
  """Connect to a Roku device, sideload an app, and take a screenshot."""
41
38
  driver = appium_driver
42
39
 
@@ -7,20 +7,17 @@ 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
 
22
- pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
23
-
24
21
 
25
22
  @pytest.mark.parametrize(
26
23
  "appium_driver",
@@ -32,7 +29,7 @@ pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
32
29
  ],
33
30
  indirect=True,
34
31
  )
35
- def test_tvos_take_screenshot(appium_driver: Any) -> None:
32
+ def test_tvos_take_screenshot(appium_driver: WebDriver) -> None:
36
33
  """Connect to a tvOS real device through the Grid and take a screenshot."""
37
34
  driver = appium_driver
38
35
 
@@ -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.9.0"
50
+ __version__ = "0.9.2"
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,9 +38,9 @@ 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, ...] = ()
42
45
  tags: dict[str, str] | None = None
43
46
 
@@ -72,23 +75,23 @@ class AllocatedDevice:
72
75
  return self.platform_label or self.platform_id
73
76
 
74
77
 
75
- 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:
76
79
  value = payload.get(key, default)
77
80
  if isinstance(value, str) and value:
78
81
  return value
79
82
  raise ValueError(f"Allocated device payload is missing {key}")
80
83
 
81
84
 
82
- def _optional_string_value(payload: dict[str, Any], key: str) -> str | None:
85
+ def _optional_string_value(payload: JsonObject, key: str) -> str | None:
83
86
  value = payload.get(key)
84
87
  return value if isinstance(value, str) and value else None
85
88
 
86
89
 
87
- def _needs_device_detail(payload: dict[str, Any]) -> bool:
90
+ def _needs_device_detail(payload: JsonObject) -> bool:
88
91
  return any(payload.get(key) is None for key in ("name", "device_type", "connection_type", "manufacturer", "model"))
89
92
 
90
93
 
91
- 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:
92
95
  merged = dict(payload)
93
96
  for key in ("name", "device_type", "connection_type", "manufacturer", "model"):
94
97
  if merged.get(key) is None and detail.get(key) is not None:
@@ -98,7 +101,7 @@ def _merge_device_detail(payload: dict[str, Any], detail: dict[str, Any]) -> dic
98
101
  return merged
99
102
 
100
103
 
101
- def _parse_unavailable_includes(payload: dict[str, Any]) -> tuple[UnavailableInclude, ...]:
104
+ def _parse_unavailable_includes(payload: JsonObject) -> tuple[UnavailableInclude, ...]:
102
105
  raw = payload.get("unavailable_includes")
103
106
  if not isinstance(raw, list):
104
107
  return ()
@@ -114,7 +117,7 @@ def _parse_unavailable_includes(payload: dict[str, Any]) -> tuple[UnavailableInc
114
117
 
115
118
 
116
119
  def hydrate_allocated_device(
117
- device_handle: dict[str, Any],
120
+ device_handle: JsonObject,
118
121
  *,
119
122
  run_id: str,
120
123
  client: GridFleetClient,
@@ -134,14 +137,14 @@ def hydrate_allocated_device(
134
137
  connection_target = _optional_string_value(payload, "connection_target")
135
138
  inline_config = payload.get("config")
136
139
  if isinstance(inline_config, dict):
137
- config: dict[str, Any] | None = inline_config
140
+ config: JsonObject | None = cast("JsonObject", inline_config)
138
141
  elif fetch_config and connection_target and "config" not in unavailable_set:
139
142
  config = client.get_device_config(connection_target)
140
143
  else:
141
144
  config = None
142
145
  inline_capabilities = payload.get("live_capabilities")
143
146
  if isinstance(inline_capabilities, dict):
144
- live_capabilities: dict[str, Any] | None = inline_capabilities
147
+ live_capabilities: JsonObject | None = cast("JsonObject", inline_capabilities)
145
148
  elif fetch_capabilities and "capabilities" not in unavailable_set:
146
149
  live_capabilities = client.get_device_capabilities(device_id)
147
150
  else:
@@ -149,7 +152,7 @@ def hydrate_allocated_device(
149
152
 
150
153
  inline_test_data = payload.get("test_data")
151
154
  if isinstance(inline_test_data, dict):
152
- test_data: dict[str, Any] | None = inline_test_data
155
+ test_data: JsonObject | None = cast("JsonObject", inline_test_data)
153
156
  elif fetch_test_data and "test_data" not in unavailable_set:
154
157
  test_data = client.get_device_test_data(device_id)
155
158
  else:
@@ -185,14 +188,14 @@ def hydrate_allocated_device(
185
188
 
186
189
  def hydrate_allocated_device_from_driver(
187
190
  allocated: AllocatedDevice,
188
- driver: Any,
191
+ driver: WebDriver,
189
192
  *,
190
193
  client: GridFleetClient,
191
194
  ) -> AllocatedDevice:
192
195
  """Refresh live capabilities from a running Appium driver session."""
193
196
  capabilities = getattr(driver, "capabilities", None)
194
197
  if isinstance(capabilities, dict):
195
- live_capabilities = dict(capabilities)
198
+ live_capabilities = cast("JsonObject", dict(capabilities))
196
199
  else:
197
200
  live_capabilities = client.get_device_capabilities(allocated.device_id)
198
201
  return replace(allocated, live_capabilities=live_capabilities)
@@ -3,35 +3,43 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
- from typing import TYPE_CHECKING, Any
6
+ from typing import TYPE_CHECKING, cast
7
7
 
8
- if TYPE_CHECKING:
9
- from collections.abc import Mapping
8
+ from appium import webdriver
9
+ from appium.options.common import AppiumOptions
10
10
 
11
11
  from .client import GridFleetClient, _default_grid_url
12
12
 
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Callable, Mapping
15
+
16
+ from appium.webdriver.webdriver import WebDriver
17
+
18
+ from .types import JsonObject
13
19
 
14
- def _catalog_payload(catalog_client: Any | None) -> dict[str, Any]:
20
+
21
+ def _catalog_payload(catalog_client: object | None) -> JsonObject:
15
22
  if catalog_client is None:
16
23
  catalog_client = GridFleetClient()
17
- if hasattr(catalog_client, "get_driver_pack_catalog"):
18
- payload = catalog_client.get_driver_pack_catalog()
24
+ catalog_getter = getattr(catalog_client, "get_driver_pack_catalog", None)
25
+ if callable(catalog_getter):
26
+ payload = catalog_getter()
19
27
  elif callable(catalog_client):
20
- payload = catalog_client()
28
+ payload = cast("Callable[[], object]", catalog_client)()
21
29
  else:
22
30
  payload = catalog_client
23
31
  if isinstance(payload, dict):
24
- return payload
32
+ return cast("JsonObject", payload)
25
33
  if isinstance(payload, list):
26
- return {"packs": payload}
34
+ return cast("JsonObject", {"packs": payload})
27
35
  raise ValueError("Driver pack catalog client returned an invalid payload")
28
36
 
29
37
 
30
- def _enabled_platform_matches(catalog: dict[str, Any], platform_id: str) -> list[tuple[dict[str, Any], dict[str, Any]]]:
38
+ def _enabled_platform_matches(catalog: JsonObject, platform_id: str) -> list[tuple[JsonObject, JsonObject]]:
31
39
  packs = catalog.get("packs")
32
40
  if not isinstance(packs, list):
33
41
  raise ValueError("Driver pack catalog payload must include a packs list")
34
- matches: list[tuple[dict[str, Any], dict[str, Any]]] = []
42
+ matches: list[tuple[JsonObject, JsonObject]] = []
35
43
  for pack in packs:
36
44
  if not isinstance(pack, dict) or pack.get("state") != "enabled":
37
45
  continue
@@ -48,8 +56,8 @@ def _resolve_pack_platform(
48
56
  *,
49
57
  pack_id: str | None,
50
58
  platform_id: str | None,
51
- catalog_client: Any | None,
52
- ) -> tuple[str, dict[str, Any]]:
59
+ catalog_client: object | None,
60
+ ) -> tuple[str, JsonObject]:
53
61
  resolved_pack_id = pack_id or os.getenv("GRIDFLEET_TESTKIT_PACK_ID")
54
62
  resolved_platform_id = platform_id or os.getenv("GRIDFLEET_TESTKIT_PLATFORM_ID")
55
63
  if not resolved_platform_id:
@@ -77,7 +85,7 @@ def _resolve_pack_platform(
77
85
  raise ValueError(f"Enabled driver pack platform for platform_id {resolved_platform_id!r} was not found")
78
86
 
79
87
 
80
- def _required_platform_string(platform: dict[str, Any], key: str) -> str:
88
+ def _required_platform_string(platform: JsonObject, key: str) -> str:
81
89
  value = platform.get(key)
82
90
  if not isinstance(value, str) or not value:
83
91
  raise ValueError(f"Driver pack platform is missing {key}")
@@ -88,15 +96,11 @@ def build_appium_options(
88
96
  *,
89
97
  pack_id: str | None = None,
90
98
  platform_id: str | None = None,
91
- capabilities: Mapping[str, Any] | None = None,
99
+ capabilities: Mapping[str, object] | None = None,
92
100
  test_name: str | None = None,
93
- catalog_client: Any | None = None,
94
- ) -> Any:
101
+ catalog_client: object | None = None,
102
+ ) -> AppiumOptions:
95
103
  """Build Appium options from driver-pack catalog platform metadata."""
96
- # appium is an optional dep (extra "appium"); imported lazily so consumers
97
- # without the extra can still use the rest of testkit.
98
- from appium.options.common import AppiumOptions # noqa: PLC0415
99
-
100
104
  params = dict(capabilities or {})
101
105
  params.setdefault("gridfleet:run_id", os.environ.get("GRIDFLEET_RUN_ID", "free"))
102
106
  explicit_platform_name = params.get("platformName")
@@ -126,16 +130,12 @@ def create_appium_driver(
126
130
  *,
127
131
  pack_id: str | None = None,
128
132
  platform_id: str | None = None,
129
- capabilities: Mapping[str, Any] | None = None,
133
+ capabilities: Mapping[str, object] | None = None,
130
134
  test_name: str | None = None,
131
135
  grid_url: str | None = None,
132
- catalog_client: Any | None = None,
133
- ) -> Any:
136
+ catalog_client: object | None = None,
137
+ ) -> WebDriver:
134
138
  """Create an Appium remote driver through Selenium Grid."""
135
- # appium is an optional dep (extra "appium"); imported lazily so consumers
136
- # without the extra can still use the rest of testkit.
137
- from appium import webdriver # noqa: PLC0415
138
-
139
139
  options = build_appium_options(
140
140
  pack_id=pack_id,
141
141
  platform_id=platform_id,
@@ -146,9 +146,9 @@ def create_appium_driver(
146
146
  return webdriver.Remote(grid_url or _default_grid_url(), options=options)
147
147
 
148
148
 
149
- def get_connection_target_from_driver(driver: Any) -> str:
149
+ def get_connection_target_from_driver(driver: WebDriver) -> str:
150
150
  """Return the runtime connection target from a live Appium driver."""
151
- capabilities = driver.capabilities
151
+ capabilities = cast("JsonObject", driver.capabilities) if isinstance(driver.capabilities, dict) else {}
152
152
  connection_target = capabilities.get("appium:udid")
153
153
  if not isinstance(connection_target, str) or not connection_target:
154
154
  raise ValueError("Could not determine device connection target from session capabilities")
@@ -156,20 +156,20 @@ def get_connection_target_from_driver(driver: Any) -> str:
156
156
 
157
157
 
158
158
  def get_device_config_for_driver(
159
- driver: Any,
159
+ driver: WebDriver,
160
160
  *,
161
161
  gridfleet_client: GridFleetClient | None = None,
162
- ) -> dict[str, Any]:
162
+ ) -> JsonObject:
163
163
  """Fetch device config for a live Appium driver using its runtime connection target."""
164
164
  client = gridfleet_client or GridFleetClient()
165
165
  return client.get_device_config(get_connection_target_from_driver(driver))
166
166
 
167
167
 
168
168
  def get_device_test_data_for_driver(
169
- driver: Any,
169
+ driver: WebDriver,
170
170
  *,
171
171
  gridfleet_client: GridFleetClient | None = None,
172
- ) -> dict[str, Any]:
172
+ ) -> JsonObject:
173
173
  """Fetch operator-attached test_data for a live Appium driver session."""
174
174
  client = gridfleet_client or GridFleetClient()
175
175
  connection_target = get_connection_target_from_driver(driver)