pymobiledevice3 5.0.4__py3-none-any.whl → 7.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. misc/understanding_idevice_protocol_layers.md +10 -5
  2. pymobiledevice3/__main__.py +171 -46
  3. pymobiledevice3/_version.py +2 -2
  4. pymobiledevice3/bonjour.py +22 -21
  5. pymobiledevice3/cli/activation.py +24 -22
  6. pymobiledevice3/cli/afc.py +49 -41
  7. pymobiledevice3/cli/amfi.py +13 -18
  8. pymobiledevice3/cli/apps.py +71 -65
  9. pymobiledevice3/cli/backup.py +134 -93
  10. pymobiledevice3/cli/bonjour.py +31 -29
  11. pymobiledevice3/cli/cli_common.py +175 -232
  12. pymobiledevice3/cli/companion_proxy.py +12 -12
  13. pymobiledevice3/cli/crash.py +95 -52
  14. pymobiledevice3/cli/developer/__init__.py +62 -0
  15. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  16. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  17. pymobiledevice3/cli/developer/arbitration.py +50 -0
  18. pymobiledevice3/cli/developer/condition.py +33 -0
  19. pymobiledevice3/cli/developer/core_device.py +294 -0
  20. pymobiledevice3/cli/developer/debugserver.py +244 -0
  21. pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
  22. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  23. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  24. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  25. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  26. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  27. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  28. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  29. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  30. pymobiledevice3/cli/idam.py +42 -0
  31. pymobiledevice3/cli/lockdown.py +70 -75
  32. pymobiledevice3/cli/mounter.py +99 -57
  33. pymobiledevice3/cli/notification.py +38 -26
  34. pymobiledevice3/cli/pcap.py +36 -20
  35. pymobiledevice3/cli/power_assertion.py +15 -16
  36. pymobiledevice3/cli/processes.py +11 -17
  37. pymobiledevice3/cli/profile.py +120 -75
  38. pymobiledevice3/cli/provision.py +27 -26
  39. pymobiledevice3/cli/remote.py +109 -100
  40. pymobiledevice3/cli/restore.py +134 -129
  41. pymobiledevice3/cli/springboard.py +50 -50
  42. pymobiledevice3/cli/syslog.py +145 -65
  43. pymobiledevice3/cli/usbmux.py +66 -27
  44. pymobiledevice3/cli/version.py +2 -5
  45. pymobiledevice3/cli/webinspector.py +232 -156
  46. pymobiledevice3/exceptions.py +6 -2
  47. pymobiledevice3/lockdown.py +5 -1
  48. pymobiledevice3/lockdown_service_provider.py +5 -0
  49. pymobiledevice3/remote/remote_service_discovery.py +18 -10
  50. pymobiledevice3/restore/device.py +28 -4
  51. pymobiledevice3/restore/restore.py +2 -2
  52. pymobiledevice3/service_connection.py +15 -12
  53. pymobiledevice3/services/afc.py +731 -220
  54. pymobiledevice3/services/device_link.py +45 -31
  55. pymobiledevice3/services/idam.py +20 -0
  56. pymobiledevice3/services/lockdown_service.py +12 -9
  57. pymobiledevice3/services/mobile_config.py +1 -0
  58. pymobiledevice3/services/mobilebackup2.py +6 -3
  59. pymobiledevice3/services/os_trace.py +97 -55
  60. pymobiledevice3/services/remote_fetch_symbols.py +13 -8
  61. pymobiledevice3/services/screenshot.py +2 -2
  62. pymobiledevice3/services/web_protocol/alert.py +8 -8
  63. pymobiledevice3/services/web_protocol/automation_session.py +87 -79
  64. pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
  65. pymobiledevice3/services/web_protocol/driver.py +71 -70
  66. pymobiledevice3/services/web_protocol/element.py +58 -56
  67. pymobiledevice3/services/web_protocol/selenium_api.py +47 -47
  68. pymobiledevice3/services/web_protocol/session_protocol.py +3 -2
  69. pymobiledevice3/services/web_protocol/switch_to.py +23 -19
  70. pymobiledevice3/services/webinspector.py +42 -67
  71. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +5 -3
  72. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/RECORD +76 -61
  73. pymobiledevice3/cli/completions.py +0 -50
  74. pymobiledevice3/cli/developer.py +0 -1539
  75. pymobiledevice3/cli/diagnostics.py +0 -110
  76. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +0 -0
  77. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
  78. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/licenses/LICENSE +0 -0
  79. {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,108 @@
1
+ import asyncio
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import Annotated, Optional
5
+
6
+ import typer
7
+ from ipsw_parser.dsc import create_device_support_layout, get_device_support_path
8
+ from packaging.version import Version
9
+ from typer_injector import InjectingTyper
10
+
11
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
12
+ from pymobiledevice3.exceptions import RSDRequiredError
13
+ from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
14
+ from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
15
+ from pymobiledevice3.services.dtfetchsymbols import DtFetchSymbols
16
+ from pymobiledevice3.services.remote_fetch_symbols import RemoteFetchSymbolsService
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ cli = InjectingTyper(
22
+ name="fetch-symbols",
23
+ help="Download the DSC (and dyld) from the device",
24
+ no_args_is_help=True,
25
+ )
26
+
27
+
28
+ async def fetch_symbols_list_task(service_provider: LockdownServiceProvider) -> None:
29
+ if Version(service_provider.product_version) < Version("17.0"):
30
+ print_json(DtFetchSymbols(service_provider).list_files())
31
+ else:
32
+ if not isinstance(service_provider, RemoteServiceDiscoveryService):
33
+ raise RSDRequiredError(service_provider.identifier)
34
+
35
+ async with RemoteFetchSymbolsService(service_provider) as fetch_symbols:
36
+ print_json([f.file_path for f in await fetch_symbols.get_dsc_file_list()])
37
+
38
+
39
+ @cli.command("list")
40
+ def fetch_symbols_list(service_provider: ServiceProviderDep) -> None:
41
+ """list of files to be downloaded"""
42
+ asyncio.run(fetch_symbols_list_task(service_provider), debug=True)
43
+
44
+
45
+ async def fetch_symbols_download_task(service_provider: LockdownServiceProvider, out: Optional[Path] = None) -> None:
46
+ should_create_device_support_layout = False
47
+ if out is None:
48
+ assert service_provider.product_type is not None # for type checker
49
+ out = get_device_support_path(
50
+ service_provider.product_type, service_provider.product_version, service_provider.product_build_version
51
+ )
52
+ should_create_device_support_layout = True
53
+
54
+ logger.info(f"Downloading DSC into: {out}")
55
+
56
+ out.mkdir(parents=True, exist_ok=True)
57
+
58
+ if Version(service_provider.product_version) < Version("17.0"):
59
+ fetch_symbols = DtFetchSymbols(service_provider)
60
+ files = fetch_symbols.list_files()
61
+
62
+ downloaded_files = set()
63
+
64
+ for i, file in enumerate(files):
65
+ if file.startswith("/"):
66
+ # trim root to allow relative download
67
+ file = file[1:]
68
+ file = out / file
69
+
70
+ if file not in downloaded_files:
71
+ # first time the file was seen in list, means we can safely remove any old copy if any
72
+ file.unlink(missing_ok=True)
73
+
74
+ downloaded_files.add(file)
75
+ file.parent.mkdir(parents=True, exist_ok=True)
76
+ with open(file, "ab") as f:
77
+ # same file may appear twice, so we'll need to append data into it
78
+ logger.info(f"writing to: {file}")
79
+ fetch_symbols.get_file(i, f)
80
+ else:
81
+ if not isinstance(service_provider, RemoteServiceDiscoveryService):
82
+ raise RSDRequiredError(service_provider.identifier)
83
+ async with RemoteFetchSymbolsService(service_provider) as fetch_symbols:
84
+ await fetch_symbols.download(out)
85
+
86
+ if should_create_device_support_layout:
87
+ assert service_provider.product_type is not None # for type checker
88
+ create_device_support_layout(
89
+ service_provider.product_type, service_provider.product_version, service_provider.product_build_version, out
90
+ )
91
+
92
+
93
+ @cli.command("download")
94
+ def fetch_symbols_download(
95
+ service_provider: ServiceProviderDep,
96
+ out: Annotated[
97
+ Optional[Path],
98
+ typer.Argument(dir_okay=True, file_okay=False),
99
+ ] = None,
100
+ ) -> None:
101
+ """
102
+ Fetches symbols from the given device and saves them into Xcode DeviceSupport directory.
103
+
104
+ This command downloads symbol data. Optionally, the user can specify an output directory where the data will
105
+ be stored. If no output directory is provided, the symbols will be downloaded into the Xcode directory directly
106
+ (DeviceSupport).
107
+ """
108
+ asyncio.run(fetch_symbols_download_task(service_provider, out), debug=True)
@@ -0,0 +1,51 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from typer_injector import InjectingTyper
7
+
8
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep
9
+ from pymobiledevice3.services.simulate_location import DtSimulateLocation
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ cli = InjectingTyper(
15
+ name="simulate-location",
16
+ help="Simulate GPS location (set, clear, or replay GPX routes).",
17
+ no_args_is_help=True,
18
+ )
19
+
20
+
21
+ @cli.command("clear")
22
+ def simulate_location_clear(service_provider: ServiceProviderDep) -> None:
23
+ """Stop location simulation and resume real GPS."""
24
+ DtSimulateLocation(service_provider).clear()
25
+
26
+
27
+ @cli.command("set")
28
+ def simulate_location_set(service_provider: ServiceProviderDep, latitude: float, longitude: float) -> None:
29
+ """
30
+ Set a fixed simulated location (latitude, longitude).
31
+ Example: `set 40.690008 -74.045843` (Liberty Island).
32
+ """
33
+ DtSimulateLocation(service_provider).set(latitude, longitude)
34
+
35
+
36
+ @cli.command("play")
37
+ def simulate_location_play(
38
+ service_provider: ServiceProviderDep,
39
+ filename: Annotated[
40
+ Path,
41
+ typer.Argument(exists=True, file_okay=True, dir_okay=False),
42
+ ],
43
+ timing_randomness_range: int,
44
+ disable_sleep: Annotated[bool, typer.Option()] = False,
45
+ ) -> None:
46
+ """Replay a GPX route; optionally disable sleeps and add timing jitter."""
47
+ DtSimulateLocation(service_provider).play_gpx_file(
48
+ str(filename),
49
+ disable_sleep=disable_sleep,
50
+ timing_randomness_range=timing_randomness_range,
51
+ )
@@ -0,0 +1,75 @@
1
+ import logging
2
+ from typing import Annotated, Optional
3
+
4
+ import typer
5
+ from typer_injector import InjectingTyper
6
+
7
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
8
+ from pymobiledevice3.cli.diagnostics import battery
9
+ from pymobiledevice3.lockdown import retry_create_using_usbmux
10
+ from pymobiledevice3.services.diagnostics import DiagnosticsService
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ cli = InjectingTyper(
16
+ name="diagnostics",
17
+ help="Reboot/Shutdown device or access other diagnostics services",
18
+ no_args_is_help=True,
19
+ )
20
+ cli.add_typer(battery.cli)
21
+
22
+
23
+ @cli.command("restart")
24
+ def diagnostics_restart(
25
+ service_provider: ServiceProviderDep,
26
+ reconnect: Annotated[
27
+ bool,
28
+ typer.Option(
29
+ "--reconnect",
30
+ "-r",
31
+ help="Wait until the device reconnects before finishing the operation.",
32
+ ),
33
+ ] = False,
34
+ ) -> None:
35
+ """Restart device"""
36
+ DiagnosticsService(lockdown=service_provider).restart()
37
+ if reconnect:
38
+ # Wait for the device to be available again
39
+ with retry_create_using_usbmux(None, serial=service_provider.udid):
40
+ print(f"Device Reconnected ({service_provider.udid}).")
41
+
42
+
43
+ @cli.command("shutdown")
44
+ def diagnostics_shutdown(service_provider: ServiceProviderDep) -> None:
45
+ """Shutdown device"""
46
+ DiagnosticsService(lockdown=service_provider).shutdown()
47
+
48
+
49
+ @cli.command("sleep")
50
+ def diagnostics_sleep(service_provider: ServiceProviderDep) -> None:
51
+ """Put device into sleep"""
52
+ DiagnosticsService(lockdown=service_provider).sleep()
53
+
54
+
55
+ @cli.command("info")
56
+ def diagnostics_info(service_provider: ServiceProviderDep) -> None:
57
+ """Get diagnostics info"""
58
+ print_json(DiagnosticsService(lockdown=service_provider).info())
59
+
60
+
61
+ @cli.command("ioregistry")
62
+ def diagnostics_ioregistry(
63
+ service_provider: ServiceProviderDep,
64
+ plane: Annotated[Optional[str], typer.Option()] = None,
65
+ name: Annotated[Optional[str], typer.Option()] = None,
66
+ ioclass: Annotated[Optional[str], typer.Option()] = None,
67
+ ) -> None:
68
+ """Get ioregistry info"""
69
+ print_json(DiagnosticsService(lockdown=service_provider).ioregistry(plane=plane, name=name, ioclass=ioclass))
70
+
71
+
72
+ @cli.command("mg")
73
+ def diagnostics_mg(service_provider: ServiceProviderDep, keys: Optional[list[str]] = None) -> None:
74
+ """Get MobileGestalt key values from given list. If empty, return all known."""
75
+ print_json(DiagnosticsService(lockdown=service_provider).mobilegestalt(keys=keys))
@@ -0,0 +1,47 @@
1
+ import logging
2
+ import time
3
+
4
+ from typer_injector import InjectingTyper
5
+
6
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
7
+ from pymobiledevice3.services.diagnostics import DiagnosticsService
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ cli = InjectingTyper(
13
+ name="battery",
14
+ help="Battery options",
15
+ no_args_is_help=True,
16
+ )
17
+
18
+
19
+ @cli.command("single")
20
+ def diagnostics_battery_single(service_provider: ServiceProviderDep) -> None:
21
+ """get single snapshot of battery data"""
22
+ raw_info = DiagnosticsService(lockdown=service_provider).get_battery()
23
+ print_json(raw_info)
24
+
25
+
26
+ @cli.command("monitor")
27
+ def diagnostics_battery_monitor(service_provider: ServiceProviderDep) -> None:
28
+ """monitor battery usage"""
29
+ diagnostics = DiagnosticsService(lockdown=service_provider)
30
+ while True:
31
+ raw_info = diagnostics.get_battery()
32
+ info = {
33
+ "InstantAmperage": raw_info.get("InstantAmperage"),
34
+ "Temperature": raw_info.get("Temperature"),
35
+ "Voltage": raw_info.get("Voltage"),
36
+ "IsCharging": raw_info.get("IsCharging"),
37
+ "CurrentCapacity": raw_info.get("CurrentCapacity"),
38
+ }
39
+ logger.info(info)
40
+ time.sleep(1)
41
+
42
+
43
+ @cli.command("wifi")
44
+ def diagnostics_wifi(service_provider: ServiceProviderDep) -> None:
45
+ """Query WiFi info from IORegistry"""
46
+ raw_info = DiagnosticsService(lockdown=service_provider).get_wifi()
47
+ print_json(raw_info)
@@ -0,0 +1,42 @@
1
+ import logging
2
+ from textwrap import dedent
3
+
4
+ from typer_injector import InjectingTyper
5
+
6
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
7
+ from pymobiledevice3.services.idam import IDAMService
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ cli = InjectingTyper(
13
+ name="idam",
14
+ help=dedent("""\
15
+ Access IDAM (Inter-Device Audio and MIDI) configuration
16
+
17
+ For more info refer to:
18
+ <https://www.youtube.com/watch?v=IXmP938brnc>
19
+ """),
20
+ no_args_is_help=True,
21
+ )
22
+
23
+
24
+ @cli.command()
25
+ def configuration_inquiry(service_provider: ServiceProviderDep) -> None:
26
+ """Inquiry IDAM configuration"""
27
+ with IDAMService(service_provider) as idam:
28
+ print_json(idam.configuration_inquiry())
29
+
30
+
31
+ @cli.command()
32
+ def enable(service_provider: ServiceProviderDep) -> None:
33
+ """Enable IDAM"""
34
+ with IDAMService(service_provider) as idam:
35
+ idam.set_idam_configuration(True)
36
+
37
+
38
+ @cli.command()
39
+ def disable(service_provider: ServiceProviderDep) -> None:
40
+ """Disable IDAM"""
41
+ with IDAMService(service_provider) as idam:
42
+ idam.set_idam_configuration(False)
@@ -2,13 +2,13 @@ import asyncio
2
2
  import logging
3
3
  import plistlib
4
4
  from pathlib import Path
5
- from typing import Optional
5
+ from typing import Annotated, Literal, Optional
6
6
 
7
- import click
7
+ import typer
8
+ from typer_injector import InjectingTyper
8
9
 
9
- from pymobiledevice3.cli.cli_common import Command, CommandWithoutAutopair, print_json, sudo_required
10
+ from pymobiledevice3.cli.cli_common import NoAutoPairServiceProviderDep, ServiceProviderDep, print_json, sudo_required
10
11
  from pymobiledevice3.cli.remote import tunnel_task
11
- from pymobiledevice3.lockdown import LockdownClient
12
12
  from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
13
13
  from pymobiledevice3.remote.common import TunnelProtocol
14
14
  from pymobiledevice3.remote.tunnel_service import CoreDeviceTunnelProxy
@@ -17,131 +17,123 @@ from pymobiledevice3.services.heartbeat import HeartbeatService
17
17
  logger = logging.getLogger(__name__)
18
18
 
19
19
 
20
- @click.group()
21
- def cli() -> None:
22
- pass
23
-
24
-
25
- @cli.group("lockdown")
26
- def lockdown_group() -> None:
27
- """Pair/Unpair device or access other lockdown services"""
28
- pass
20
+ cli = InjectingTyper(
21
+ name="lockdown",
22
+ help="Pair/Unpair device or access other lockdown services",
23
+ no_args_is_help=True,
24
+ )
29
25
 
30
26
 
31
- @lockdown_group.command("recovery", cls=Command)
32
- def lockdown_recovery(service_provider: LockdownClient):
27
+ @cli.command("recovery")
28
+ def lockdown_recovery(service_provider: ServiceProviderDep) -> None:
33
29
  """enter recovery"""
34
30
  print_json(service_provider.enter_recovery())
35
31
 
36
32
 
37
- @lockdown_group.command("service", cls=Command)
38
- @click.argument("service_name")
39
- def lockdown_service(service_provider: LockdownServiceProvider, service_name):
33
+ @cli.command("service")
34
+ def lockdown_service(service_provider: ServiceProviderDep, service_name: str) -> None:
40
35
  """send-receive raw service messages with a given service name"""
41
36
  service_provider.start_lockdown_service(service_name).shell()
42
37
 
43
38
 
44
- @lockdown_group.command("developer-service", cls=Command)
45
- @click.argument("service_name")
46
- def lockdown_developer_service(service_provider: LockdownServiceProvider, service_name):
39
+ @cli.command("developer-service")
40
+ def lockdown_developer_service(service_provider: ServiceProviderDep, service_name: str) -> None:
47
41
  """send-receive raw service messages with a given developer service name"""
48
42
  service_provider.start_lockdown_developer_service(service_name).shell()
49
43
 
50
44
 
51
- @lockdown_group.command("info", cls=Command)
52
- def lockdown_info(service_provider: LockdownServiceProvider):
45
+ @cli.command("info")
46
+ def lockdown_info(service_provider: ServiceProviderDep) -> None:
53
47
  """query all lockdown values"""
54
48
  print_json(service_provider.all_values)
55
49
 
56
50
 
57
- @lockdown_group.command("get", cls=Command)
58
- @click.argument("domain", required=False)
59
- @click.argument("key", required=False)
60
- def lockdown_get(service_provider: LockdownClient, domain, key):
51
+ @cli.command("get")
52
+ def lockdown_get(service_provider: ServiceProviderDep, domain: Optional[str] = None, key: Optional[str] = None) -> None:
61
53
  """query lockdown values by their domain and key names"""
62
54
  print_json(service_provider.get_value(domain=domain, key=key))
63
55
 
64
56
 
65
- @lockdown_group.command("set", cls=Command)
66
- @click.argument("value")
67
- @click.argument("domain", required=False)
68
- @click.argument("key", required=False)
69
- def lockdown_set(service_provider: LockdownClient, value, domain, key):
57
+ @cli.command("set")
58
+ def lockdown_set(
59
+ service_provider: ServiceProviderDep,
60
+ value: str,
61
+ domain: Optional[str] = None,
62
+ key: Optional[str] = None,
63
+ ) -> None:
70
64
  """set a lockdown value using python's eval()"""
71
65
  print_json(service_provider.set_value(value=eval(value), domain=domain, key=key))
72
66
 
73
67
 
74
- @lockdown_group.command("remove", cls=Command)
75
- @click.argument("domain")
76
- @click.argument("key")
77
- def lockdown_remove(service_provider: LockdownClient, domain, key):
68
+ @cli.command("remove")
69
+ def lockdown_remove(service_provider: ServiceProviderDep, domain: str, key: str) -> None:
78
70
  """remove a domain/key pair"""
79
71
  print_json(service_provider.remove_value(domain=domain, key=key))
80
72
 
81
73
 
82
- @lockdown_group.command("unpair", cls=CommandWithoutAutopair)
83
- @click.argument("host_id", required=False)
84
- def lockdown_unpair(service_provider: LockdownClient, host_id: Optional[str] = None):
74
+ @cli.command("unpair")
75
+ def lockdown_unpair(service_provider: NoAutoPairServiceProviderDep, host_id: Optional[str] = None) -> None:
85
76
  """unpair from connected device"""
86
77
  service_provider.unpair(host_id=host_id)
87
78
 
88
79
 
89
- @lockdown_group.command("pair", cls=CommandWithoutAutopair)
90
- def lockdown_pair(service_provider: LockdownClient):
80
+ @cli.command("pair")
81
+ def lockdown_pair(service_provider: NoAutoPairServiceProviderDep) -> None:
91
82
  """pair device"""
92
83
  service_provider.pair()
93
84
 
94
85
 
95
- @lockdown_group.command("pair-supervised", cls=CommandWithoutAutopair)
96
- @click.argument("keybag", type=click.Path(file_okay=True, dir_okay=False, exists=True))
97
- def lockdown_pair_supervised(service_provider: LockdownClient, keybag: str) -> None:
86
+ @cli.command("pair-supervised")
87
+ def lockdown_pair_supervised(
88
+ service_provider: NoAutoPairServiceProviderDep,
89
+ keybag: Annotated[
90
+ Path,
91
+ typer.Argument(file_okay=True, dir_okay=False, exists=True),
92
+ ],
93
+ ) -> None:
98
94
  """pair supervised device"""
99
- service_provider.pair_supervised(Path(keybag))
95
+ service_provider.pair_supervised(keybag)
100
96
 
101
97
 
102
- @lockdown_group.command("save-pair-record", cls=CommandWithoutAutopair)
103
- @click.argument("output", type=click.File("wb"))
104
- def lockdown_save_pair_record(service_provider: LockdownClient, output):
98
+ @cli.command("save-pair-record")
99
+ def lockdown_save_pair_record(service_provider: NoAutoPairServiceProviderDep, output: Path) -> None:
105
100
  """save pair record to specified location"""
106
101
  if service_provider.pair_record is None:
107
102
  logger.error("no pairing record was found")
108
103
  return
109
- plistlib.dump(service_provider.pair_record, output)
104
+ output.write_bytes(plistlib.dumps(service_provider.pair_record))
110
105
 
111
106
 
112
- @lockdown_group.command("date", cls=Command)
113
- def lockdown_date(service_provider: LockdownClient):
107
+ @cli.command("date")
108
+ def lockdown_date(service_provider: ServiceProviderDep) -> None:
114
109
  """get device date"""
115
110
  print(service_provider.date)
116
111
 
117
112
 
118
- @lockdown_group.command("heartbeat", cls=Command)
119
- def lockdown_heartbeat(service_provider: LockdownClient):
113
+ @cli.command("heartbeat")
114
+ def lockdown_heartbeat(service_provider: ServiceProviderDep) -> None:
120
115
  """start heartbeat service"""
121
116
  HeartbeatService(service_provider).start()
122
117
 
123
118
 
124
- @lockdown_group.command("language", cls=Command)
125
- @click.argument("language", required=False)
126
- def lockdown_language(service_provider: LockdownClient, language: Optional[str]) -> None:
119
+ @cli.command("language")
120
+ def lockdown_language(service_provider: ServiceProviderDep, language: Optional[str] = None) -> None:
127
121
  """Get/Set current language settings"""
128
122
  if language is not None:
129
123
  service_provider.set_language(language)
130
124
  print_json(service_provider.language)
131
125
 
132
126
 
133
- @lockdown_group.command("locale", cls=Command)
134
- @click.argument("locale", required=False)
135
- def lockdown_locale(service_provider: LockdownClient, locale: Optional[str]) -> None:
127
+ @cli.command("locale")
128
+ def lockdown_locale(service_provider: ServiceProviderDep, locale: Optional[str] = None) -> None:
136
129
  """Get/Set current language settings"""
137
130
  if locale is not None:
138
131
  service_provider.set_locale(locale)
139
132
  print_json(service_provider.locale)
140
133
 
141
134
 
142
- @lockdown_group.command("device-name", cls=Command)
143
- @click.argument("new_name", required=False)
144
- def lockdown_device_name(service_provider: LockdownClient, new_name):
135
+ @cli.command("device-name")
136
+ def lockdown_device_name(service_provider: ServiceProviderDep, new_name: Optional[str] = None) -> None:
145
137
  """get/set current device name"""
146
138
  if new_name:
147
139
  service_provider.set_value(new_name, key="DeviceName")
@@ -149,9 +141,10 @@ def lockdown_device_name(service_provider: LockdownClient, new_name):
149
141
  print(f"{service_provider.get_value(key='DeviceName')}")
150
142
 
151
143
 
152
- @lockdown_group.command("wifi-connections", cls=Command)
153
- @click.argument("state", type=click.Choice(["on", "off"]), required=False)
154
- def lockdown_wifi_connections(service_provider: LockdownClient, state):
144
+ @cli.command("wifi-connections")
145
+ def lockdown_wifi_connections(
146
+ service_provider: ServiceProviderDep, state: Optional[Literal["on", "off"]] = None
147
+ ) -> None:
155
148
  """get/set wifi connections state"""
156
149
  if not state:
157
150
  # show current state
@@ -170,21 +163,23 @@ async def async_cli_start_tunnel(service_provider: LockdownServiceProvider, scri
170
163
  )
171
164
 
172
165
 
173
- @lockdown_group.command("start-tunnel", cls=Command)
174
- @click.option(
175
- "--script-mode",
176
- is_flag=True,
177
- help="Show only HOST and port number to allow easy parsing from external shell scripts",
178
- )
166
+ @cli.command("start-tunnel")
179
167
  @sudo_required
180
- def cli_start_tunnel(service_provider: LockdownServiceProvider, script_mode: bool) -> None:
168
+ def cli_start_tunnel(
169
+ service_provider: ServiceProviderDep,
170
+ script_mode: Annotated[
171
+ bool,
172
+ typer.Option(help="Show only HOST and port number to allow easy parsing from external shell scripts"),
173
+ ] = False,
174
+ ) -> None:
181
175
  """start tunnel"""
182
176
  asyncio.run(async_cli_start_tunnel(service_provider, script_mode), debug=True)
183
177
 
184
178
 
185
- @lockdown_group.command("assistive-touch", cls=Command)
186
- @click.argument("state", type=click.Choice(["on", "off"]), required=False)
187
- def lockdown_assistive_touch(service_provider: LockdownClient, state: str) -> None:
179
+ @cli.command("assistive-touch")
180
+ def lockdown_assistive_touch(
181
+ service_provider: ServiceProviderDep, state: Optional[Literal["on", "off"]] = None
182
+ ) -> None:
188
183
  """get/set assistive touch icon state (visibility)"""
189
184
  if not state:
190
185
  key = "AssistiveTouchEnabledByiTunes"