ruyi 0.44.0a20251118__py3-none-any.whl → 0.45.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 (55) hide show
  1. ruyi/__main__.py +16 -4
  2. ruyi/cli/cmd.py +6 -5
  3. ruyi/cli/config_cli.py +14 -11
  4. ruyi/cli/main.py +34 -17
  5. ruyi/cli/oobe.py +10 -10
  6. ruyi/cli/self_cli.py +49 -36
  7. ruyi/cli/user_input.py +42 -12
  8. ruyi/cli/version_cli.py +11 -5
  9. ruyi/config/__init__.py +30 -10
  10. ruyi/config/errors.py +19 -7
  11. ruyi/device/provision.py +116 -55
  12. ruyi/device/provision_cli.py +6 -3
  13. ruyi/i18n/__init__.py +129 -0
  14. ruyi/log/__init__.py +6 -5
  15. ruyi/mux/runtime.py +19 -6
  16. ruyi/mux/venv/maker.py +93 -35
  17. ruyi/mux/venv/venv_cli.py +13 -10
  18. ruyi/pluginhost/plugin_cli.py +4 -3
  19. ruyi/resource_bundle/__init__.py +22 -8
  20. ruyi/resource_bundle/__main__.py +6 -5
  21. ruyi/resource_bundle/data.py +13 -9
  22. ruyi/ruyipkg/admin_checksum.py +4 -1
  23. ruyi/ruyipkg/admin_cli.py +9 -6
  24. ruyi/ruyipkg/augmented_pkg.py +15 -14
  25. ruyi/ruyipkg/checksum.py +8 -2
  26. ruyi/ruyipkg/distfile.py +33 -9
  27. ruyi/ruyipkg/entity.py +12 -2
  28. ruyi/ruyipkg/entity_cli.py +20 -12
  29. ruyi/ruyipkg/entity_provider.py +11 -2
  30. ruyi/ruyipkg/fetcher.py +38 -9
  31. ruyi/ruyipkg/install.py +163 -64
  32. ruyi/ruyipkg/install_cli.py +18 -15
  33. ruyi/ruyipkg/list.py +27 -20
  34. ruyi/ruyipkg/list_cli.py +12 -7
  35. ruyi/ruyipkg/news.py +23 -11
  36. ruyi/ruyipkg/news_cli.py +10 -7
  37. ruyi/ruyipkg/profile_cli.py +8 -2
  38. ruyi/ruyipkg/repo.py +22 -8
  39. ruyi/ruyipkg/unpack.py +42 -8
  40. ruyi/ruyipkg/unpack_method.py +5 -1
  41. ruyi/ruyipkg/update_cli.py +8 -3
  42. ruyi/telemetry/aggregate.py +5 -0
  43. ruyi/telemetry/provider.py +292 -105
  44. ruyi/telemetry/store.py +68 -15
  45. ruyi/telemetry/telemetry_cli.py +32 -13
  46. ruyi/utils/git.py +18 -11
  47. ruyi/utils/prereqs.py +10 -5
  48. ruyi/utils/ssl_patch.py +2 -1
  49. ruyi/version.py +9 -3
  50. {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/METADATA +4 -2
  51. ruyi-0.45.0.dist-info/RECORD +103 -0
  52. {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/WHEEL +1 -1
  53. ruyi-0.44.0a20251118.dist-info/RECORD +0 -102
  54. {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/entry_points.txt +0 -0
  55. {ruyi-0.44.0a20251118.dist-info → ruyi-0.45.0.dist-info}/licenses/LICENSE-Apache.txt +0 -0
ruyi/cli/user_input.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import os.path
2
2
 
3
+ from ..i18n import _
3
4
  from ..log import RuyiLogger
4
5
 
5
6
 
@@ -10,7 +11,7 @@ def pause_before_continuing(
10
11
 
11
12
  EOFError should be handled by the caller."""
12
13
 
13
- logger.stdout("Press [green]<ENTER>[/] to continue: ", end="")
14
+ logger.stdout(_("Press [green]<ENTER>[/] to continue: "), end="")
14
15
  input()
15
16
 
16
17
 
@@ -26,9 +27,11 @@ def ask_for_yesno_confirmation(
26
27
  logger.stdout(f"{prompt} {choices_help} ", end="")
27
28
  user_input = input()
28
29
  except EOFError:
29
- yesno = "YES" if default else "NO"
30
+ yesno = _("YES") if default else _("NO")
30
31
  logger.W(
31
- f"EOF while reading user input, assuming the default choice {yesno}"
32
+ _(
33
+ "EOF while reading user input, assuming the default choice {yesno}"
34
+ ).format(yesno=yesno)
32
35
  )
33
36
  return default
34
37
 
@@ -39,8 +42,12 @@ def ask_for_yesno_confirmation(
39
42
  if user_input in {"N", "n", "no"}:
40
43
  return False
41
44
  else:
42
- logger.stdout(f"Unrecognized input [yellow]'{user_input}'[/].")
43
- logger.stdout("Accepted choices: Y/y/yes for YES, N/n/no for NO.")
45
+ logger.stdout(
46
+ _("Unrecognized input [yellow]'{user_input}'[/].").format(
47
+ user_input=user_input
48
+ )
49
+ )
50
+ logger.stdout(_("Accepted choices: Y/y/yes for YES, N/n/no for NO."))
44
51
 
45
52
 
46
53
  def ask_for_kv_choice(
@@ -81,12 +88,19 @@ def ask_for_choice(
81
88
  if default_idx is not None:
82
89
  if not (0 <= default_idx < nr_choices):
83
90
  raise ValueError(f"Default choice index {default_idx} out of range")
84
- choices_help = f"(1-{nr_choices}, default {default_idx + 1})"
91
+ choices_help = _("(1-{nr_choices}, default {default})").format(
92
+ nr_choices=nr_choices,
93
+ default=default_idx + 1,
94
+ )
85
95
  else:
86
- choices_help = f"(1-{nr_choices})"
96
+ choices_help = _("(1-{nr_choices})").format(nr_choices=nr_choices)
87
97
  while True:
88
98
  try:
89
- user_input = input(f"Choice? {choices_help} ")
99
+ user_input = input(
100
+ _("Choice? {choices_help} ").format(
101
+ choices_help=choices_help,
102
+ )
103
+ )
90
104
  except EOFError:
91
105
  raise ValueError("EOF while reading user choice")
92
106
 
@@ -96,18 +110,34 @@ def ask_for_choice(
96
110
  try:
97
111
  choice_int = int(user_input)
98
112
  except ValueError:
99
- logger.stdout(f"Unrecognized input [yellow]'{user_input}'[/].")
100
113
  logger.stdout(
101
- f"Accepted choices: an integer number from 1 to {nr_choices} inclusive."
114
+ _("Unrecognized input [yellow]'{user_input}'[/].").format(
115
+ user_input=user_input,
116
+ )
117
+ )
118
+ logger.stdout(
119
+ _(
120
+ "Accepted choices: an integer number from 1 to {nr_choices} inclusive."
121
+ ).format(
122
+ nr_choices=nr_choices,
123
+ )
102
124
  )
103
125
  continue
104
126
 
105
127
  if 1 <= choice_int <= nr_choices:
106
128
  return choice_int - 1
107
129
 
108
- logger.stdout(f"Out-of-range input [yellow]'{user_input}'[/].")
109
130
  logger.stdout(
110
- f"Accepted choices: an integer number from 1 to {nr_choices} inclusive."
131
+ _("Out-of-range input [yellow]'{user_input}'[/].").format(
132
+ user_input=user_input,
133
+ )
134
+ )
135
+ logger.stdout(
136
+ _(
137
+ "Accepted choices: an integer number from 1 to {nr_choices} inclusive."
138
+ ).format(
139
+ nr_choices=nr_choices,
140
+ )
111
141
  )
112
142
 
113
143
 
ruyi/cli/version_cli.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import argparse
2
2
  from typing import TYPE_CHECKING
3
3
 
4
+ from ..i18n import _
4
5
  from .cmd import RootCommand
5
6
 
6
7
  if TYPE_CHECKING:
@@ -11,7 +12,7 @@ if TYPE_CHECKING:
11
12
  class VersionCommand(
12
13
  RootCommand,
13
14
  cmd="version",
14
- help="Print version information",
15
+ help=_("Print version information"),
15
16
  ):
16
17
  @classmethod
17
18
  def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
@@ -27,19 +28,24 @@ def cli_version(cfg: "GlobalConfig", args: argparse.Namespace) -> int:
27
28
  from ..ruyipkg.host import get_native_host
28
29
  from ..version import COPYRIGHT_NOTICE, MPL_REDIST_NOTICE, RUYI_SEMVER
29
30
 
30
- print(f"Ruyi {RUYI_SEMVER}\n\nRunning on {get_native_host()}.")
31
+ print(
32
+ _("Ruyi {version}\n\nRunning on {host}.").format(
33
+ version=RUYI_SEMVER,
34
+ host=get_native_host(),
35
+ )
36
+ )
31
37
 
32
38
  if cfg.is_installation_externally_managed:
33
- print("This Ruyi installation is externally managed.")
39
+ print(_("This Ruyi installation is externally managed."))
34
40
 
35
41
  print()
36
42
 
37
- cfg.logger.stdout(COPYRIGHT_NOTICE)
43
+ cfg.logger.stdout(_(COPYRIGHT_NOTICE))
38
44
 
39
45
  # Output the MPL notice only when we actually bundle and depend on the
40
46
  # MPL component(s), which right now is only certifi. Keep the condition
41
47
  # synced with __main__.py.
42
48
  if hasattr(ruyi, "__compiled__") and ruyi.__compiled__.standalone:
43
- cfg.logger.stdout(MPL_REDIST_NOTICE)
49
+ cfg.logger.stdout(_(MPL_REDIST_NOTICE))
44
50
 
45
51
  return 0
ruyi/config/__init__.py CHANGED
@@ -18,6 +18,13 @@ if TYPE_CHECKING:
18
18
  from ..utils.xdg_basedir import XDGPathEntry
19
19
  from .news import NewsReadStatusStore
20
20
 
21
+ import babel
22
+ # not sure why Pyright insists on individual imports
23
+ # otherwise, at the use site (`except babel.core.UnknownLocaleError`):
24
+ # error: "core" is not a known attribute of module "babel" (reportAttributeAccessIssue)
25
+ from babel.core import UnknownLocaleError
26
+
27
+ from ..i18n import _
21
28
  from . import errors
22
29
  from . import schema
23
30
 
@@ -115,7 +122,11 @@ class GlobalConfig:
115
122
  if iem is not None and not is_global_scope:
116
123
  iem_cfg_key = f"{schema.SECTION_INSTALLATION}.{schema.KEY_INSTALLATION_EXTERNALLY_MANAGED}"
117
124
  self.logger.W(
118
- f"the config key [yellow]{iem_cfg_key}[/] cannot be set from user config; ignoring",
125
+ _(
126
+ "the config key [yellow]{key}[/] cannot be set from user config; ignoring"
127
+ ).format(
128
+ key=iem_cfg_key,
129
+ ),
119
130
  )
120
131
  else:
121
132
  self.is_installation_externally_managed = bool(iem)
@@ -133,7 +144,11 @@ class GlobalConfig:
133
144
  if self.override_repo_dir:
134
145
  if not pathlib.Path(self.override_repo_dir).is_absolute():
135
146
  self.logger.W(
136
- f"the local repo path '{self.override_repo_dir}' is not absolute; ignoring"
147
+ _(
148
+ "the local repo path '{path}' is not absolute; ignoring"
149
+ ).format(
150
+ path=self.override_repo_dir,
151
+ )
137
152
  )
138
153
  self.override_repo_dir = None
139
154
 
@@ -274,6 +289,15 @@ class GlobalConfig:
274
289
  def lang_code(self) -> str:
275
290
  return self._lang_code
276
291
 
292
+ @cached_property
293
+ def babel_locale(self) -> babel.Locale:
294
+ try:
295
+ return babel.Locale.parse(self.lang_code)
296
+ except UnknownLocaleError:
297
+ # this can happen in case of unrecognized locale names, which
298
+ # apparently falls back to "C"
299
+ return babel.Locale.parse("en_US")
300
+
277
301
  @property
278
302
  def cache_root(self) -> os.PathLike[Any]:
279
303
  return self._dirs.app_cache
@@ -297,17 +321,13 @@ class GlobalConfig:
297
321
  def telemetry_root(self) -> os.PathLike[Any]:
298
322
  return pathlib.Path(self.ensure_state_dir()) / "telemetry"
299
323
 
300
- @property
301
- def telemetry(self) -> "TelemetryProvider | None":
302
- return None if self.telemetry_mode == "off" else self._telemetry_provider
303
-
304
324
  @cached_property
305
- def _telemetry_provider(self) -> "TelemetryProvider | None":
306
- """Do not access directly; use the ``telemetry`` property instead."""
307
-
325
+ def telemetry(self) -> "TelemetryProvider":
308
326
  from ..telemetry.provider import TelemetryProvider
309
327
 
310
- return None if self.telemetry_mode == "off" else TelemetryProvider(self)
328
+ # for allowing minimal uploads when telemetry is off
329
+ minimal_mode = self.telemetry_mode == "off"
330
+ return TelemetryProvider(self, minimal_mode)
311
331
 
312
332
  @property
313
333
  def telemetry_mode(self) -> str:
ruyi/config/errors.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from os import PathLike
2
2
  from typing import Any, Sequence
3
3
 
4
+ from ..i18n import _
5
+
4
6
 
5
7
  class InvalidConfigSectionError(Exception):
6
8
  def __init__(self, section: str) -> None:
@@ -8,7 +10,7 @@ class InvalidConfigSectionError(Exception):
8
10
  self._section = section
9
11
 
10
12
  def __str__(self) -> str:
11
- return f"invalid config section: {self._section}"
13
+ return _("invalid config section: {section}").format(section=self._section)
12
14
 
13
15
  def __repr__(self) -> str:
14
16
  return f"InvalidConfigSectionError({self._section!r})"
@@ -20,7 +22,7 @@ class InvalidConfigKeyError(Exception):
20
22
  self._key = key
21
23
 
22
24
  def __str__(self) -> str:
23
- return f"invalid config key: {self._key}"
25
+ return _("invalid config key: {key}").format(key=self._key)
24
26
 
25
27
  def __repr__(self) -> str:
26
28
  return f"InvalidConfigKeyError({self._key:!r})"
@@ -39,7 +41,13 @@ class InvalidConfigValueTypeError(TypeError):
39
41
  self._expected = expected
40
42
 
41
43
  def __str__(self) -> str:
42
- return f"invalid value type for config key {self._key}: {type(self._val)}, expected {self._expected}"
44
+ return _(
45
+ "invalid value type for config key {key}: {actual_type}, expected {expected_type}"
46
+ ).format(
47
+ key=self._key,
48
+ actual_type=type(self._val),
49
+ expected_type=self._expected,
50
+ )
43
51
 
44
52
  def __repr__(self) -> str:
45
53
  return f"InvalidConfigValueTypeError({self._key!r}, {self._val!r}, {self._expected:!r})"
@@ -58,8 +66,10 @@ class InvalidConfigValueError(ValueError):
58
66
  self._typ = typ
59
67
 
60
68
  def __str__(self) -> str:
61
- return (
62
- f"invalid config value for key {self._key} (type {self._typ}): {self._val}"
69
+ return _("invalid config value for key {key} (type {typ}): {val}").format(
70
+ key=self._key,
71
+ typ=self._typ,
72
+ val=self._val,
63
73
  )
64
74
 
65
75
  def __repr__(self) -> str:
@@ -74,7 +84,7 @@ class MalformedConfigFileError(Exception):
74
84
  self._path = path
75
85
 
76
86
  def __str__(self) -> str:
77
- return f"malformed config file: {self._path}"
87
+ return _("malformed config file: {path}").format(path=self._path)
78
88
 
79
89
  def __repr__(self) -> str:
80
90
  return f"MalformedConfigFileError({self._path:!r})"
@@ -86,7 +96,9 @@ class ProtectedGlobalConfigError(Exception):
86
96
  self._key = key
87
97
 
88
98
  def __str__(self) -> str:
89
- return f"attempt to modify protected global config key: {self._key}"
99
+ return _("attempt to modify protected global config key: {key}").format(
100
+ key=self._key,
101
+ )
90
102
 
91
103
  def __repr__(self) -> str:
92
104
  return f"ProtectedGlobalConfigError({self._key!r})"