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
@@ -1,38 +1,33 @@
1
1
  import logging
2
2
 
3
- import click
3
+ from typer_injector import InjectingTyper
4
4
 
5
- from pymobiledevice3.cli.cli_common import Command, print_json
6
- from pymobiledevice3.lockdown import LockdownClient
5
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
7
6
  from pymobiledevice3.services.amfi import AmfiService
8
7
 
9
8
  logger = logging.getLogger(__name__)
10
9
 
11
10
 
12
- @click.group()
13
- def cli() -> None:
14
- pass
11
+ cli = InjectingTyper(
12
+ name="amfi",
13
+ help="Enable developer-mode or query its state",
14
+ no_args_is_help=True,
15
+ )
15
16
 
16
17
 
17
- @cli.group()
18
- def amfi() -> None:
19
- """Enable developer-mode or query its state"""
20
- pass
21
-
22
-
23
- @amfi.command(cls=Command)
24
- def reveal_developer_mode(service_provider: LockdownClient):
18
+ @cli.command()
19
+ def reveal_developer_mode(service_provider: ServiceProviderDep) -> None:
25
20
  """reveal developer mode option in device's UI"""
26
21
  AmfiService(service_provider).reveal_developer_mode_option_in_ui()
27
22
 
28
23
 
29
- @amfi.command(cls=Command)
30
- def enable_developer_mode(service_provider: LockdownClient):
24
+ @cli.command()
25
+ def enable_developer_mode(service_provider: ServiceProviderDep) -> None:
31
26
  """enable developer mode"""
32
27
  AmfiService(service_provider).enable_developer_mode()
33
28
 
34
29
 
35
- @amfi.command(cls=Command)
36
- def developer_mode_status(service_provider: LockdownClient):
30
+ @cli.command()
31
+ def developer_mode_status(service_provider: ServiceProviderDep) -> None:
37
32
  """query developer mode status"""
38
33
  print_json(service_provider.developer_mode_status)
@@ -1,35 +1,37 @@
1
- import click
1
+ from pathlib import Path
2
+ from typing import Annotated, Literal
2
3
 
3
- from pymobiledevice3.cli.cli_common import Command, print_json
4
- from pymobiledevice3.lockdown import LockdownClient
5
- from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
4
+ import typer
5
+ from typer_injector import InjectingTyper
6
+
7
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
6
8
  from pymobiledevice3.services.house_arrest import HouseArrestService
7
9
  from pymobiledevice3.services.installation_proxy import InstallationProxyService
8
10
 
9
-
10
- @click.group()
11
- def cli() -> None:
12
- pass
13
-
14
-
15
- @cli.group()
16
- def apps() -> None:
17
- """Manage installed applications"""
18
- pass
11
+ cli = InjectingTyper(
12
+ name="apps",
13
+ help="List, query, install, uninstall, and inspect apps on the device.",
14
+ no_args_is_help=True,
15
+ )
19
16
 
20
17
 
21
- @apps.command("list", cls=Command)
22
- @click.option(
23
- "app_type",
24
- "-t",
25
- "--type",
26
- type=click.Choice(["System", "User", "Hidden", "Any"]),
27
- default="Any",
28
- help="include only applications of given type",
29
- )
30
- @click.option("--calculate-sizes/--no-calculate-size", default=False)
31
- def apps_list(service_provider: LockdownServiceProvider, app_type: str, calculate_sizes: bool) -> None:
32
- """list installed apps"""
18
+ @cli.command("list")
19
+ def apps_list(
20
+ service_provider: ServiceProviderDep,
21
+ app_type: Annotated[
22
+ Literal["System", "User", "Hidden", "Any"],
23
+ typer.Option(
24
+ "--type",
25
+ "-t",
26
+ help="Filter by application type (System/User/Hidden/Any).",
27
+ ),
28
+ ] = "Any",
29
+ calculate_sizes: Annotated[
30
+ bool,
31
+ typer.Option(help="Include app size information (slower)."),
32
+ ] = False,
33
+ ) -> None:
34
+ """List installed apps."""
33
35
  print_json(
34
36
  InstallationProxyService(lockdown=service_provider).get_apps(
35
37
  application_type=app_type, calculate_sizes=calculate_sizes
@@ -37,11 +39,16 @@ def apps_list(service_provider: LockdownServiceProvider, app_type: str, calculat
37
39
  )
38
40
 
39
41
 
40
- @apps.command("query", cls=Command)
41
- @click.argument("bundle_identifiers", nargs=-1)
42
- @click.option("--calculate-sizes/--no-calculate-size", default=False)
43
- def apps_query(service_provider: LockdownServiceProvider, bundle_identifiers: list[str], calculate_sizes: bool) -> None:
44
- """query installed apps"""
42
+ @cli.command("query")
43
+ def apps_query(
44
+ service_provider: ServiceProviderDep,
45
+ bundle_identifiers: list[str],
46
+ calculate_sizes: Annotated[
47
+ bool,
48
+ typer.Option(help="Include app size information (slower)."),
49
+ ] = False,
50
+ ) -> None:
51
+ """Return metadata for specific bundle identifiers."""
45
52
  print_json(
46
53
  InstallationProxyService(lockdown=service_provider).get_apps(
47
54
  calculate_sizes=calculate_sizes, bundle_identifiers=bundle_identifiers
@@ -49,50 +56,49 @@ def apps_query(service_provider: LockdownServiceProvider, bundle_identifiers: li
49
56
  )
50
57
 
51
58
 
52
- @apps.command("uninstall", cls=Command)
53
- @click.argument("bundle_id")
54
- def uninstall(service_provider: LockdownClient, bundle_id):
55
- """uninstall app by given bundle_id"""
59
+ @cli.command("uninstall")
60
+ def uninstall(service_provider: ServiceProviderDep, bundle_id: str) -> None:
61
+ """Uninstall an app by bundle identifier."""
56
62
  InstallationProxyService(lockdown=service_provider).uninstall(bundle_id)
57
63
 
58
64
 
59
- @apps.command("install", cls=Command)
60
- @click.option("--developer", is_flag=True, help="Install developer package")
61
- @click.argument("package", type=click.Path(exists=True))
62
- def install(service_provider: LockdownServiceProvider, package: str, developer: bool) -> None:
63
- """install given .ipa/.app/.ipcc"""
65
+ @cli.command("install")
66
+ def install(
67
+ service_provider: ServiceProviderDep,
68
+ package: Annotated[
69
+ Path,
70
+ typer.Argument(exists=True),
71
+ ],
72
+ developer: Annotated[
73
+ bool,
74
+ typer.Option(help="Install developer package"),
75
+ ] = False,
76
+ ) -> None:
77
+ """Install a local .ipa/.app/.ipcc package."""
64
78
  InstallationProxyService(lockdown=service_provider).install_from_local(package, developer=developer)
65
79
 
66
80
 
67
- @apps.command("afc", cls=Command)
68
- @click.option("--documents", is_flag=True)
69
- @click.argument("bundle_id")
70
- def afc(service_provider: LockdownClient, bundle_id: str, documents: bool):
71
- """open an AFC shell for given bundle_id, assuming its profile is installed"""
81
+ @cli.command("afc")
82
+ def afc(
83
+ service_provider: ServiceProviderDep, bundle_id: str, documents: Annotated[bool, typer.Option()] = False
84
+ ) -> None:
85
+ """Open an AFC shell into the app container; pass --documents for Documents-only."""
72
86
  HouseArrestService(lockdown=service_provider, bundle_id=bundle_id, documents_only=documents).shell()
73
87
 
74
88
 
75
- @apps.command("pull", cls=Command)
76
- @click.argument("bundle_id")
77
- @click.argument("remote_file", type=click.Path(exists=False))
78
- @click.argument("local_file", type=click.Path(exists=False))
79
- def pull(service_provider: LockdownClient, bundle_id: str, remote_file: str, local_file: str):
80
- """pull remote file from specified bundle_id"""
81
- HouseArrestService(lockdown=service_provider, bundle_id=bundle_id).pull(remote_file, local_file)
89
+ @cli.command("pull")
90
+ def pull(service_provider: ServiceProviderDep, bundle_id: str, remote_file: Path, local_file: Path) -> None:
91
+ """Pull a file from an app container to a local path."""
92
+ HouseArrestService(lockdown=service_provider, bundle_id=bundle_id).pull(str(remote_file), str(local_file))
82
93
 
83
94
 
84
- @apps.command("push", cls=Command)
85
- @click.argument("bundle_id")
86
- @click.argument("local_file", type=click.Path(exists=False))
87
- @click.argument("remote_file", type=click.Path(exists=False))
88
- def push(service_provider: LockdownClient, bundle_id: str, local_file: str, remote_file: str):
89
- """push local file into specified bundle_id"""
90
- HouseArrestService(lockdown=service_provider, bundle_id=bundle_id).push(local_file, remote_file)
95
+ @cli.command("push")
96
+ def push(service_provider: ServiceProviderDep, bundle_id: str, local_file: Path, remote_file: Path) -> None:
97
+ """Push a local file into an app container."""
98
+ HouseArrestService(lockdown=service_provider, bundle_id=bundle_id).push(str(local_file), str(remote_file))
91
99
 
92
100
 
93
- @apps.command("rm", cls=Command)
94
- @click.argument("bundle_id")
95
- @click.argument("remote_file", type=click.Path(exists=False))
96
- def rm(service_provider: LockdownClient, bundle_id: str, remote_file: str):
97
- """remove remote file from specified bundle_id"""
98
- HouseArrestService(lockdown=service_provider, bundle_id=bundle_id).rm(remote_file)
101
+ @cli.command("rm")
102
+ def rm(service_provider: ServiceProviderDep, bundle_id: str, remote_file: Path) -> None:
103
+ """Delete a file from an app container."""
104
+ HouseArrestService(lockdown=service_provider, bundle_id=bundle_id).rm(str(remote_file))
@@ -1,42 +1,65 @@
1
1
  import logging
2
+ from pathlib import Path
3
+ from typing import Annotated, Literal
2
4
 
3
- import click
5
+ import typer
4
6
  from tqdm import tqdm
7
+ from typer_injector import InjectingTyper
5
8
 
6
- from pymobiledevice3.cli.cli_common import Command
7
- from pymobiledevice3.lockdown import LockdownClient
8
- from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
9
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep
9
10
  from pymobiledevice3.services.mobilebackup2 import Mobilebackup2Service
10
11
 
11
- source_option = click.option("--source", default="", help="The UDID of the source device.")
12
- password_option = click.option("-p", "--password", default="", help="Backup password.")
13
- backup_directory_arg = click.argument("backup-directory", type=click.Path(exists=True, file_okay=False))
14
- backup_directory_option = click.option(
15
- "-b", "--backup-directory", type=click.Path(exists=True, file_okay=False), default="."
16
- )
17
-
18
12
  logger = logging.getLogger(__name__)
19
13
 
20
14
 
21
- @click.group()
22
- def cli() -> None:
23
- pass
24
-
25
-
26
- @cli.group()
27
- def backup2() -> None:
28
- """Backup/Restore options"""
29
- pass
15
+ cli = InjectingTyper(
16
+ name="backup2",
17
+ help="Create, inspect, and restore MobileBackup2 backups.",
18
+ no_args_is_help=True,
19
+ )
30
20
 
31
21
 
32
- @backup2.command(cls=Command)
33
- @click.argument("backup-directory", type=click.Path(file_okay=False))
34
- @click.option(
35
- "--full",
36
- is_flag=True,
37
- help=("Whether to do a full backup. If full is True, any previous backup attempts will be discarded."),
38
- )
39
- def backup(service_provider: LockdownServiceProvider, backup_directory: str, full: bool) -> None:
22
+ SourceOption = Annotated[
23
+ str,
24
+ typer.Option(help="The UDID of the source device."),
25
+ ]
26
+ PasswordOption = Annotated[
27
+ str,
28
+ typer.Option(
29
+ "--password",
30
+ "-p",
31
+ help="Backup password.",
32
+ ),
33
+ ]
34
+ BackupDirectoryArg = Annotated[
35
+ Path,
36
+ typer.Argument(
37
+ exists=True,
38
+ file_okay=False,
39
+ ),
40
+ ]
41
+ BackupDirectoryOption = Annotated[
42
+ Path,
43
+ typer.Option(
44
+ "--backup-directory",
45
+ "-b",
46
+ exists=True,
47
+ file_okay=False,
48
+ ),
49
+ ]
50
+
51
+
52
+ @cli.command()
53
+ def backup(
54
+ service_provider: ServiceProviderDep,
55
+ backup_directory: BackupDirectoryArg,
56
+ full: Annotated[
57
+ bool,
58
+ typer.Option(
59
+ help="Whether to do a full backup. If full is True, any previous backup attempts will be discarded.",
60
+ ),
61
+ ],
62
+ ) -> None:
40
63
  """
41
64
  Backup device.
42
65
 
@@ -45,34 +68,43 @@ def backup(service_provider: LockdownServiceProvider, backup_directory: str, ful
45
68
  backup_client = Mobilebackup2Service(service_provider)
46
69
  with tqdm(total=100, dynamic_ncols=True) as pbar:
47
70
 
48
- def update_bar(percentage):
71
+ def update_bar(percentage) -> None:
49
72
  pbar.n = percentage
50
73
  pbar.refresh()
51
74
 
52
- backup_client.backup(full=full, backup_directory=backup_directory, progress_callback=update_bar)
75
+ backup_client.backup(full=full, backup_directory=str(backup_directory), progress_callback=update_bar)
53
76
 
54
77
 
55
- @backup2.command(cls=Command)
56
- @backup_directory_arg
57
- @click.option("--system/--no-system", default=False, help="Restore system files.")
58
- @click.option("--reboot/--no-reboot", default=True, help="Reboot the device when done.")
59
- @click.option("--copy/--no-copy", default=False, help="Create a copy of backup folder before restoring.")
60
- @click.option("--settings/--no-settings", default=True, help="Restore device settings.")
61
- @click.option("--remove/--no-remove", default=False, help="Remove items which aren't being restored.")
62
- @click.option("--skip-apps", is_flag=True, help="Do not trigger re-installation of apps after restore")
63
- @password_option
64
- @source_option
78
+ @cli.command()
65
79
  def restore(
66
- service_provider: LockdownServiceProvider,
67
- backup_directory: str,
68
- system: bool,
69
- reboot: bool,
70
- copy: bool,
71
- settings: bool,
72
- remove: bool,
73
- skip_apps: bool,
74
- password: str,
75
- source: str,
80
+ service_provider: ServiceProviderDep,
81
+ backup_directory: BackupDirectoryArg,
82
+ system: Annotated[
83
+ bool,
84
+ typer.Option(help="Restore system files."),
85
+ ] = False,
86
+ reboot: Annotated[
87
+ bool,
88
+ typer.Option(help="Reboot the device when done."),
89
+ ] = True,
90
+ copy: Annotated[
91
+ bool,
92
+ typer.Option(help="Create a copy of backup folder before restoring."),
93
+ ] = False,
94
+ settings: Annotated[
95
+ bool,
96
+ typer.Option(help="Restore device settings."),
97
+ ] = True,
98
+ remove: Annotated[
99
+ bool,
100
+ typer.Option(help="Remove items which aren't being restored."),
101
+ ] = False,
102
+ skip_apps: Annotated[
103
+ bool,
104
+ typer.Option(help="Do not trigger re-installation of apps after restore."),
105
+ ] = False,
106
+ password: PasswordOption = "",
107
+ source: SourceOption = "",
76
108
  ) -> None:
77
109
  """
78
110
  Restore a backup to a device.
@@ -82,12 +114,12 @@ def restore(
82
114
  backup_client = Mobilebackup2Service(service_provider)
83
115
  with tqdm(total=100, dynamic_ncols=True) as pbar:
84
116
 
85
- def update_bar(percentage):
117
+ def update_bar(percentage) -> None:
86
118
  pbar.n = percentage
87
119
  pbar.refresh()
88
120
 
89
121
  backup_client.restore(
90
- backup_directory=backup_directory,
122
+ backup_directory=str(backup_directory),
91
123
  progress_callback=update_bar,
92
124
  system=system,
93
125
  reboot=reboot,
@@ -100,47 +132,49 @@ def restore(
100
132
  )
101
133
 
102
134
 
103
- @backup2.command(cls=Command)
104
- @backup_directory_arg
105
- @source_option
106
- def info(service_provider: LockdownClient, backup_directory, source):
135
+ @cli.command()
136
+ def info(service_provider: ServiceProviderDep, backup_directory: BackupDirectoryArg, source: SourceOption = "") -> None:
107
137
  """
108
138
  Print information about a backup.
109
139
  """
110
140
  backup_client = Mobilebackup2Service(service_provider)
111
- print(backup_client.info(backup_directory=backup_directory, source=source))
141
+ print(backup_client.info(backup_directory=str(backup_directory), source=source))
112
142
 
113
143
 
114
- @backup2.command("list", cls=Command)
115
- @backup_directory_arg
116
- @source_option
117
- def list_(service_provider: LockdownClient, backup_directory, source):
144
+ @cli.command("list")
145
+ def list_(
146
+ service_provider: ServiceProviderDep, backup_directory: BackupDirectoryArg, source: SourceOption = ""
147
+ ) -> None:
118
148
  """
119
149
  List all file in the backup in a CSV format.
120
150
  """
121
151
  backup_client = Mobilebackup2Service(service_provider)
122
- print(backup_client.list(backup_directory=backup_directory, source=source))
152
+ print(backup_client.list(backup_directory=str(backup_directory), source=source))
123
153
 
124
154
 
125
- @backup2.command(cls=Command)
126
- @backup_directory_arg
127
- @password_option
128
- @source_option
129
- def unback(service_provider: LockdownClient, backup_directory, password, source):
155
+ @cli.command()
156
+ def unback(
157
+ service_provider: ServiceProviderDep,
158
+ backup_directory: BackupDirectoryArg,
159
+ password: PasswordOption = "",
160
+ source: SourceOption = "",
161
+ ) -> None:
130
162
  """
131
163
  Convert all files in the backup to the correct directory hierarchy.
132
164
  """
133
165
  backup_client = Mobilebackup2Service(service_provider)
134
- backup_client.unback(backup_directory=backup_directory, password=password, source=source)
166
+ backup_client.unback(backup_directory=str(backup_directory), password=password, source=source)
135
167
 
136
168
 
137
- @backup2.command(cls=Command)
138
- @click.argument("domain-name")
139
- @click.argument("relative-path")
140
- @backup_directory_arg
141
- @password_option
142
- @source_option
143
- def extract(service_provider: LockdownClient, domain_name, relative_path, backup_directory, password, source):
169
+ @cli.command()
170
+ def extract(
171
+ service_provider: ServiceProviderDep,
172
+ domain_name: str,
173
+ relative_path: str,
174
+ backup_directory: BackupDirectoryArg,
175
+ password: PasswordOption = "",
176
+ source: SourceOption = "",
177
+ ) -> None:
144
178
  """
145
179
  Extract a file from the backup.
146
180
 
@@ -149,15 +183,21 @@ def extract(service_provider: LockdownClient, domain_name, relative_path, backup
149
183
  """
150
184
  backup_client = Mobilebackup2Service(service_provider)
151
185
  backup_client.extract(
152
- domain_name, relative_path, backup_directory=backup_directory, password=password, source=source
186
+ domain_name, relative_path, backup_directory=str(backup_directory), password=password, source=source
153
187
  )
154
188
 
155
189
 
156
- @backup2.command(cls=Command)
157
- @click.argument("mode", type=click.Choice(["on", "off"], case_sensitive=False))
158
- @click.argument("password")
159
- @backup_directory_option
160
- def encryption(service_provider: LockdownClient, backup_directory, mode, password):
190
+ @cli.command()
191
+ def encryption(
192
+ service_provider: ServiceProviderDep,
193
+ *,
194
+ backup_directory: BackupDirectoryOption = Path("."),
195
+ mode: Annotated[
196
+ Literal["on", "off"],
197
+ typer.Argument(case_sensitive=False),
198
+ ],
199
+ password: str,
200
+ ) -> None:
161
201
  """
162
202
  Set backup encryption on / off.
163
203
 
@@ -170,16 +210,18 @@ def encryption(service_provider: LockdownClient, backup_directory, mode, passwor
170
210
  logger.error("Encryption already " + ("on!" if should_encrypt else "off!"))
171
211
  return
172
212
  if should_encrypt:
173
- backup_client.change_password(backup_directory, new=password)
213
+ backup_client.change_password(str(backup_directory), new=password)
174
214
  else:
175
- backup_client.change_password(backup_directory, old=password)
215
+ backup_client.change_password(str(backup_directory), old=password)
176
216
 
177
217
 
178
- @backup2.command(cls=Command)
179
- @click.argument("old-password")
180
- @click.argument("new-password")
181
- @backup_directory_option
182
- def change_password(service_provider: LockdownClient, old_password, new_password, backup_directory):
218
+ @cli.command()
219
+ def change_password(
220
+ service_provider: ServiceProviderDep,
221
+ old_password: str,
222
+ new_password: str,
223
+ backup_directory: BackupDirectoryOption = Path("."),
224
+ ) -> None:
183
225
  """
184
226
  Change the backup password.
185
227
  """
@@ -187,14 +229,13 @@ def change_password(service_provider: LockdownClient, old_password, new_password
187
229
  if not backup_client.will_encrypt:
188
230
  logger.error("Encryption is not turned on!")
189
231
  return
190
- backup_client.change_password(backup_directory, old=old_password, new=new_password)
232
+ backup_client.change_password(str(backup_directory), old=old_password, new=new_password)
191
233
 
192
234
 
193
- @backup2.command(cls=Command)
194
- @backup_directory_arg
195
- def erase_device(service_provider: LockdownClient, backup_directory):
235
+ @cli.command()
236
+ def erase_device(service_provider: ServiceProviderDep, backup_directory: BackupDirectoryArg) -> None:
196
237
  """
197
238
  Erase all data on the device.
198
239
  """
199
240
  backup_client = Mobilebackup2Service(service_provider)
200
- backup_client.erase_device(backup_directory)
241
+ backup_client.erase_device(str(backup_directory))
@@ -1,26 +1,23 @@
1
1
  import asyncio
2
- from typing import Optional
2
+ from pathlib import Path
3
+ from typing import Annotated, Optional
3
4
 
4
- import click
5
+ import typer
6
+ from typer_injector import InjectingTyper
5
7
 
6
8
  from pymobiledevice3.bonjour import DEFAULT_BONJOUR_TIMEOUT, browse_remotepairing, browse_remotepairing_manual_pairing
7
- from pymobiledevice3.cli.cli_common import BaseCommand, print_json
9
+ from pymobiledevice3.cli.cli_common import print_json
8
10
  from pymobiledevice3.cli.remote import browse_rsd
9
11
  from pymobiledevice3.lockdown import get_mobdev2_lockdowns
10
12
 
11
-
12
- @click.group()
13
- def cli() -> None:
14
- pass
15
-
16
-
17
- @cli.group("bonjour")
18
- def bonjour_cli() -> None:
19
- """Browse devices over bonjour"""
20
- pass
13
+ cli = InjectingTyper(
14
+ name="bonjour",
15
+ help="Browse devices over bonjour",
16
+ no_args_is_help=True,
17
+ )
21
18
 
22
19
 
23
- async def cli_mobdev2_task(timeout: float, pair_records: Optional[str]) -> None:
20
+ async def cli_mobdev2_task(timeout: float, pair_records: Optional[Path]) -> None:
24
21
  output = []
25
22
  async for ip, lockdown in get_mobdev2_lockdowns(timeout=timeout, pair_records=pair_records):
26
23
  short_info = lockdown.short_info
@@ -29,14 +26,19 @@ async def cli_mobdev2_task(timeout: float, pair_records: Optional[str]) -> None:
29
26
  print_json(output)
30
27
 
31
28
 
32
- @bonjour_cli.command("mobdev2", cls=BaseCommand)
33
- @click.option("--timeout", default=DEFAULT_BONJOUR_TIMEOUT, type=click.INT)
34
- @click.option(
35
- "--pair-records",
36
- type=click.Path(dir_okay=True, file_okay=False, exists=True),
37
- help="pair records to attempt validation with",
38
- )
39
- def cli_mobdev2(timeout: float, pair_records: Optional[str]) -> None:
29
+ @cli.command("mobdev2")
30
+ def cli_mobdev2(
31
+ timeout: Annotated[float, typer.Option()] = DEFAULT_BONJOUR_TIMEOUT,
32
+ pair_records: Annotated[
33
+ Optional[Path],
34
+ typer.Option(
35
+ exists=True,
36
+ dir_okay=True,
37
+ file_okay=True,
38
+ help="pair records to attempt validation with",
39
+ ),
40
+ ] = None,
41
+ ) -> None:
40
42
  """browse for mobdev2 devices over bonjour"""
41
43
  asyncio.run(cli_mobdev2_task(timeout, pair_records))
42
44
 
@@ -49,9 +51,8 @@ async def cli_remotepairing_task(timeout: float) -> None:
49
51
  print_json(output)
50
52
 
51
53
 
52
- @bonjour_cli.command("remotepairing", cls=BaseCommand)
53
- @click.option("--timeout", default=DEFAULT_BONJOUR_TIMEOUT, type=click.FLOAT)
54
- def cli_remotepairing(timeout: float) -> None:
54
+ @cli.command("remotepairing")
55
+ def cli_remotepairing(timeout: Annotated[float, typer.Option()] = DEFAULT_BONJOUR_TIMEOUT) -> None:
55
56
  """browse for remotepairing devices over bonjour (without attempting pair verification)"""
56
57
  asyncio.run(cli_remotepairing_task(timeout=timeout))
57
58
 
@@ -68,9 +69,10 @@ async def cli_remotepairing_manual_pairing_task(timeout: float) -> None:
68
69
  print_json(output)
69
70
 
70
71
 
71
- @bonjour_cli.command("remotepairing-manual-pairing", cls=BaseCommand)
72
- @click.option("--timeout", default=DEFAULT_BONJOUR_TIMEOUT, type=click.FLOAT)
73
- def cli_remotepairing_manual_pairing(timeout: float) -> None:
72
+ @cli.command("remotepairing-manual-pairing")
73
+ def cli_remotepairing_manual_pairing(
74
+ timeout: Annotated[float, typer.Option()] = DEFAULT_BONJOUR_TIMEOUT,
75
+ ) -> None:
74
76
  """browse for remotepairing-manual-pairing devices over bonjour"""
75
77
  asyncio.run(cli_remotepairing_manual_pairing_task(timeout=timeout))
76
78
 
@@ -79,7 +81,7 @@ async def cli_browse_rsd() -> None:
79
81
  print_json(await browse_rsd())
80
82
 
81
83
 
82
- @bonjour_cli.command("rsd", cls=BaseCommand)
84
+ @cli.command("rsd")
83
85
  def cli_rsd() -> None:
84
86
  """browse RemoteXPC devices using bonjour"""
85
87
  asyncio.run(cli_browse_rsd(), debug=True)