pymobiledevice3 6.2.0__py3-none-any.whl → 7.0.0__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 (61) hide show
  1. pymobiledevice3/__main__.py +136 -44
  2. pymobiledevice3/_version.py +2 -2
  3. pymobiledevice3/bonjour.py +19 -20
  4. pymobiledevice3/cli/activation.py +24 -22
  5. pymobiledevice3/cli/afc.py +49 -41
  6. pymobiledevice3/cli/amfi.py +13 -18
  7. pymobiledevice3/cli/apps.py +71 -65
  8. pymobiledevice3/cli/backup.py +134 -93
  9. pymobiledevice3/cli/bonjour.py +31 -29
  10. pymobiledevice3/cli/cli_common.py +179 -232
  11. pymobiledevice3/cli/companion_proxy.py +12 -12
  12. pymobiledevice3/cli/crash.py +95 -52
  13. pymobiledevice3/cli/developer/__init__.py +62 -0
  14. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  15. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  16. pymobiledevice3/cli/developer/arbitration.py +50 -0
  17. pymobiledevice3/cli/developer/condition.py +33 -0
  18. pymobiledevice3/cli/developer/core_device.py +294 -0
  19. pymobiledevice3/cli/developer/debugserver.py +244 -0
  20. pymobiledevice3/cli/developer/dvt/__init__.py +387 -0
  21. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  22. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  23. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  24. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  25. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  26. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  27. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  28. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  29. pymobiledevice3/cli/idam.py +18 -22
  30. pymobiledevice3/cli/lockdown.py +70 -75
  31. pymobiledevice3/cli/mounter.py +99 -57
  32. pymobiledevice3/cli/notification.py +38 -26
  33. pymobiledevice3/cli/pcap.py +36 -20
  34. pymobiledevice3/cli/power_assertion.py +15 -16
  35. pymobiledevice3/cli/processes.py +11 -17
  36. pymobiledevice3/cli/profile.py +120 -75
  37. pymobiledevice3/cli/provision.py +27 -26
  38. pymobiledevice3/cli/remote.py +108 -99
  39. pymobiledevice3/cli/restore.py +134 -129
  40. pymobiledevice3/cli/springboard.py +50 -50
  41. pymobiledevice3/cli/syslog.py +138 -74
  42. pymobiledevice3/cli/usbmux.py +66 -27
  43. pymobiledevice3/cli/version.py +2 -5
  44. pymobiledevice3/cli/webinspector.py +149 -103
  45. pymobiledevice3/remote/remote_service_discovery.py +11 -10
  46. pymobiledevice3/restore/device.py +28 -4
  47. pymobiledevice3/service_connection.py +1 -1
  48. pymobiledevice3/services/mobilebackup2.py +4 -1
  49. pymobiledevice3/services/screenshot.py +2 -2
  50. pymobiledevice3/services/web_protocol/automation_session.py +4 -2
  51. pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
  52. pymobiledevice3/services/web_protocol/element.py +3 -3
  53. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/METADATA +3 -2
  54. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/RECORD +58 -45
  55. pymobiledevice3/cli/completions.py +0 -50
  56. pymobiledevice3/cli/developer.py +0 -1645
  57. pymobiledevice3/cli/diagnostics.py +0 -110
  58. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/WHEEL +0 -0
  59. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/entry_points.txt +0 -0
  60. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/licenses/LICENSE +0 -0
  61. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/top_level.txt +0 -0
@@ -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"
@@ -2,18 +2,19 @@ import asyncio
2
2
  import logging
3
3
  from functools import update_wrapper
4
4
  from pathlib import Path
5
+ from typing import Annotated, Optional
5
6
  from urllib.error import URLError
6
7
 
7
- import click
8
+ import typer
9
+ from typer_injector import InjectingTyper
8
10
 
9
- from pymobiledevice3.cli.cli_common import Command, print_json
11
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
10
12
  from pymobiledevice3.exceptions import (
11
13
  AlreadyMountedError,
12
14
  DeveloperDiskImageNotFoundError,
13
15
  NotMountedError,
14
16
  UnsupportedCommandError,
15
17
  )
16
- from pymobiledevice3.lockdown import LockdownClient
17
18
  from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
18
19
  from pymobiledevice3.services.mobile_image_mounter import (
19
20
  DeveloperDiskImageMounter,
@@ -37,19 +38,15 @@ def catch_errors(func):
37
38
  return update_wrapper(catch_function, func)
38
39
 
39
40
 
40
- @click.group()
41
- def cli() -> None:
42
- pass
43
-
44
-
45
- @cli.group()
46
- def mounter() -> None:
47
- """Mount/Umount DeveloperDiskImage or query related info"""
48
- pass
41
+ cli = InjectingTyper(
42
+ name="mounter",
43
+ help="Mount/Umount DeveloperDiskImage or query related info",
44
+ no_args_is_help=True,
45
+ )
49
46
 
50
47
 
51
- @mounter.command("list", cls=Command)
52
- def mounter_list(service_provider: LockdownClient):
48
+ @cli.command("list")
49
+ def mounter_list(service_provider: ServiceProviderDep) -> None:
53
50
  """list all mounted images"""
54
51
  output = []
55
52
 
@@ -63,9 +60,8 @@ def mounter_list(service_provider: LockdownClient):
63
60
  print_json(output)
64
61
 
65
62
 
66
- @mounter.command("lookup", cls=Command)
67
- @click.argument("image_type")
68
- def mounter_lookup(service_provider: LockdownClient, image_type):
63
+ @cli.command("lookup")
64
+ def mounter_lookup(service_provider: ServiceProviderDep, image_type: str) -> None:
69
65
  """lookup mounter image type"""
70
66
  try:
71
67
  signature = MobileImageMounterService(lockdown=service_provider).lookup_image(image_type)
@@ -74,9 +70,9 @@ def mounter_lookup(service_provider: LockdownClient, image_type):
74
70
  logger.error(f"Disk image of type: {image_type} is not mounted")
75
71
 
76
72
 
77
- @mounter.command("umount-developer", cls=Command)
73
+ @cli.command("umount-developer")
78
74
  @catch_errors
79
- def mounter_umount_developer(service_provider: LockdownClient):
75
+ def mounter_umount_developer(service_provider: ServiceProviderDep) -> None:
80
76
  """unmount Developer image"""
81
77
  try:
82
78
  DeveloperDiskImageMounter(lockdown=service_provider).umount()
@@ -85,9 +81,9 @@ def mounter_umount_developer(service_provider: LockdownClient):
85
81
  logger.error("Developer image isn't currently mounted")
86
82
 
87
83
 
88
- @mounter.command("umount-personalized", cls=Command)
84
+ @cli.command("umount-personalized")
89
85
  @catch_errors
90
- def mounter_umount_personalized(service_provider: LockdownClient):
86
+ def mounter_umount_personalized(service_provider: ServiceProviderDep) -> None:
91
87
  """unmount Personalized image"""
92
88
  try:
93
89
  PersonalizedImageMounter(lockdown=service_provider).umount()
@@ -96,13 +92,29 @@ def mounter_umount_personalized(service_provider: LockdownClient):
96
92
  logger.error("Personalized image isn't currently mounted")
97
93
 
98
94
 
99
- @mounter.command("mount-developer", cls=Command)
100
- @click.argument("image", type=click.Path(exists=True, file_okay=True, dir_okay=False))
101
- @click.argument("signature", type=click.Path(exists=True, file_okay=True, dir_okay=False))
95
+ @cli.command("mount-developer")
102
96
  @catch_errors
103
- def mounter_mount_developer(service_provider: LockdownServiceProvider, image: str, signature: str) -> None:
97
+ def mounter_mount_developer(
98
+ service_provider: ServiceProviderDep,
99
+ image: Annotated[
100
+ Path,
101
+ typer.Argument(
102
+ exists=True,
103
+ file_okay=True,
104
+ dir_okay=False,
105
+ ),
106
+ ],
107
+ signature: Annotated[
108
+ Path,
109
+ typer.Argument(
110
+ exists=True,
111
+ file_okay=True,
112
+ dir_okay=False,
113
+ ),
114
+ ],
115
+ ) -> None:
104
116
  """mount developer image"""
105
- DeveloperDiskImageMounter(lockdown=service_provider).mount(Path(image), Path(signature))
117
+ DeveloperDiskImageMounter(lockdown=service_provider).mount(image, signature)
106
118
  logger.info("Developer image mounted successfully")
107
119
 
108
120
 
@@ -115,16 +127,39 @@ async def mounter_mount_personalized_task(
115
127
  logger.info("Personalized image mounted successfully")
116
128
 
117
129
 
118
- @mounter.command("mount-personalized", cls=Command)
119
- @click.argument("image", type=click.Path(exists=True, file_okay=True, dir_okay=False))
120
- @click.argument("trust-cache", type=click.Path(exists=True, file_okay=True, dir_okay=False))
121
- @click.argument("build-manifest", type=click.Path(exists=True, file_okay=True, dir_okay=False))
130
+ @cli.command("mount-personalized")
122
131
  @catch_errors
123
132
  def mounter_mount_personalized(
124
- service_provider: LockdownServiceProvider, image: str, trust_cache: str, build_manifest: str
133
+ service_provider: ServiceProviderDep,
134
+ image: Annotated[
135
+ Path,
136
+ typer.Argument(
137
+ exists=True,
138
+ file_okay=True,
139
+ dir_okay=False,
140
+ ),
141
+ ],
142
+ trust_cache: Annotated[
143
+ Path,
144
+ typer.Argument(
145
+ exists=True,
146
+ file_okay=True,
147
+ dir_okay=False,
148
+ ),
149
+ ],
150
+ build_manifest: Annotated[
151
+ Path,
152
+ typer.Argument(
153
+ exists=True,
154
+ file_okay=True,
155
+ dir_okay=False,
156
+ ),
157
+ ],
125
158
  ) -> None:
126
159
  """mount personalized image"""
127
- asyncio.run(mounter_mount_personalized_task(service_provider, image, trust_cache, build_manifest), debug=True)
160
+ asyncio.run(
161
+ mounter_mount_personalized_task(service_provider, str(image), str(trust_cache), str(build_manifest)), debug=True
162
+ )
128
163
 
129
164
 
130
165
  async def mounter_auto_mount_task(service_provider: LockdownServiceProvider, xcode: str, version: str) -> None:
@@ -144,42 +179,49 @@ async def mounter_auto_mount_task(service_provider: LockdownServiceProvider, xco
144
179
  )
145
180
 
146
181
 
147
- @mounter.command("auto-mount", cls=Command)
148
- @click.option(
149
- "-x",
150
- "--xcode",
151
- type=click.Path(exists=True, dir_okay=True, file_okay=False),
152
- help="Xcode application path used to figure out automatically the DeveloperDiskImage path",
153
- )
154
- @click.option(
155
- "--version", help="use a different DeveloperDiskImage version from the one retrieved by lockdownconnection"
156
- )
157
- def mounter_auto_mount(service_provider: LockdownServiceProvider, xcode: str, version: str) -> None:
182
+ @cli.command("auto-mount")
183
+ def mounter_auto_mount(
184
+ service_provider: ServiceProviderDep,
185
+ xcode: Annotated[
186
+ Optional[Path],
187
+ typer.Option(
188
+ "--xcode",
189
+ "-x",
190
+ exists=True,
191
+ file_okay=True,
192
+ dir_okay=False,
193
+ help="Xcode application path used to figure out automatically the DeveloperDiskImage path",
194
+ ),
195
+ ] = None,
196
+ version: Annotated[
197
+ Optional[str],
198
+ typer.Option(help="Use a different DeveloperDiskImage version from the one retrieved by lockdownconnection"),
199
+ ] = None,
200
+ ) -> None:
158
201
  """auto-detect correct DeveloperDiskImage and mount it"""
159
- asyncio.run(mounter_auto_mount_task(service_provider, xcode, version), debug=True)
202
+ asyncio.run(mounter_auto_mount_task(service_provider, str(xcode), version), debug=True)
160
203
 
161
204
 
162
- @mounter.command("query-developer-mode-status", cls=Command)
163
- def mounter_query_developer_mode_status(service_provider: LockdownClient):
205
+ @cli.command("query-developer-mode-status")
206
+ def mounter_query_developer_mode_status(service_provider: ServiceProviderDep) -> None:
164
207
  """Query developer mode status"""
165
208
  print_json(MobileImageMounterService(lockdown=service_provider).query_developer_mode_status())
166
209
 
167
210
 
168
- @mounter.command("query-nonce", cls=Command)
169
- @click.option("--image-type")
170
- def mounter_query_nonce(service_provider: LockdownClient, image_type: str):
211
+ @cli.command("query-nonce")
212
+ def mounter_query_nonce(service_provider: ServiceProviderDep, image_type: Annotated[str, typer.Option()]) -> None:
171
213
  """Query nonce"""
172
214
  print_json(MobileImageMounterService(lockdown=service_provider).query_nonce(image_type))
173
215
 
174
216
 
175
- @mounter.command("query-personalization-identifiers", cls=Command)
176
- def mounter_query_personalization_identifiers(service_provider: LockdownClient):
217
+ @cli.command("query-personalization-identifiers")
218
+ def mounter_query_personalization_identifiers(service_provider: ServiceProviderDep) -> None:
177
219
  """Query personalization identifiers"""
178
220
  print_json(MobileImageMounterService(lockdown=service_provider).query_personalization_identifiers())
179
221
 
180
222
 
181
- @mounter.command("query-personalization-manifest", cls=Command)
182
- def mounter_query_personalization_manifest(service_provider: LockdownClient):
223
+ @cli.command("query-personalization-manifest")
224
+ def mounter_query_personalization_manifest(service_provider: ServiceProviderDep) -> None:
183
225
  """Query personalization manifest"""
184
226
  result = []
185
227
  mounter = MobileImageMounterService(lockdown=service_provider)
@@ -188,12 +230,12 @@ def mounter_query_personalization_manifest(service_provider: LockdownClient):
188
230
  print_json(result)
189
231
 
190
232
 
191
- @mounter.command("roll-personalization-nonce", cls=Command)
192
- def mounter_roll_personalization_nonce(service_provider: LockdownClient):
233
+ @cli.command("roll-personalization-nonce")
234
+ def mounter_roll_personalization_nonce(service_provider: ServiceProviderDep) -> None:
193
235
  MobileImageMounterService(lockdown=service_provider).roll_personalization_nonce()
194
236
 
195
237
 
196
- @mounter.command("roll-cryptex-nonce", cls=Command)
197
- def mounter_roll_cryptex_nonce(service_provider: LockdownClient):
238
+ @cli.command("roll-cryptex-nonce")
239
+ def mounter_roll_cryptex_nonce(service_provider: ServiceProviderDep) -> None:
198
240
  """Roll cryptex nonce (will reboot)"""
199
241
  MobileImageMounterService(lockdown=service_provider).roll_cryptex_nonce()
@@ -1,41 +1,48 @@
1
1
  import logging
2
+ from typing import Annotated
2
3
 
3
- import click
4
+ import typer
5
+ from typer_injector import InjectingTyper
4
6
 
5
- from pymobiledevice3.cli.cli_common import Command
6
- from pymobiledevice3.lockdown import LockdownClient
7
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep
7
8
  from pymobiledevice3.resources.firmware_notifications import get_notifications
8
9
  from pymobiledevice3.services.notification_proxy import NotificationProxyService
9
10
 
10
11
  logger = logging.getLogger(__name__)
11
12
 
12
13
 
13
- @click.group()
14
- def cli() -> None:
15
- pass
14
+ cli = InjectingTyper(
15
+ name="notifications",
16
+ help="Post or observe Darwin notifications via notification_proxy.",
17
+ no_args_is_help=True,
18
+ )
16
19
 
17
20
 
18
- @cli.group()
19
- def notification() -> None:
20
- """Post/Observe notifications"""
21
- pass
22
-
23
-
24
- @notification.command(cls=Command)
25
- @click.argument("names", nargs=-1)
26
- @click.option("--insecure", is_flag=True, help="use the insecure relay meant for untrusted clients instead")
27
- def post(service_provider: LockdownClient, names, insecure):
28
- """API for notify_post()."""
21
+ @cli.command()
22
+ def post(
23
+ service_provider: ServiceProviderDep,
24
+ names: list[str],
25
+ insecure: Annotated[
26
+ bool,
27
+ typer.Option(help="Use the insecure relay meant for untrusted clients instead of the trusted channel."),
28
+ ],
29
+ ) -> None:
30
+ """Post one or more Darwin notifications (notify_post)."""
29
31
  service = NotificationProxyService(lockdown=service_provider, insecure=insecure)
30
32
  for name in names:
31
33
  service.notify_post(name)
32
34
 
33
35
 
34
- @notification.command(cls=Command)
35
- @click.argument("names", nargs=-1)
36
- @click.option("--insecure", is_flag=True, help="use the insecure relay meant for untrusted clients instead")
37
- def observe(service_provider: LockdownClient, names, insecure):
38
- """API for notify_register_dispatch()."""
36
+ @cli.command()
37
+ def observe(
38
+ service_provider: ServiceProviderDep,
39
+ names: list[str],
40
+ insecure: Annotated[
41
+ bool,
42
+ typer.Option(help="Use the insecure relay meant for untrusted clients instead of the trusted channel."),
43
+ ],
44
+ ) -> None:
45
+ """Subscribe and stream notifications (notify_register_dispatch)."""
39
46
  service = NotificationProxyService(lockdown=service_provider, insecure=insecure)
40
47
  for name in names:
41
48
  service.notify_register_dispatch(name)
@@ -44,10 +51,15 @@ def observe(service_provider: LockdownClient, names, insecure):
44
51
  logger.info(event)
45
52
 
46
53
 
47
- @notification.command("observe-all", cls=Command)
48
- @click.option("--insecure", is_flag=True, help="use the insecure relay meant for untrusted clients instead")
49
- def observe_all(service_provider: LockdownClient, insecure):
50
- """attempt to observe all builtin firmware notifications."""
54
+ @cli.command("observe-all")
55
+ def observe_all(
56
+ service_provider: ServiceProviderDep,
57
+ insecure: Annotated[
58
+ bool,
59
+ typer.Option(help="Use the insecure relay meant for untrusted clients instead of the trusted channel."),
60
+ ],
61
+ ) -> None:
62
+ """Subscribe to all known firmware notifications and stream events."""
51
63
  service = NotificationProxyService(lockdown=service_provider, insecure=insecure)
52
64
  for notification in get_notifications():
53
65
  service.notify_register_dispatch(notification)