ruyi 0.44.0__tar.gz → 0.44.0a20251118__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/PKG-INFO +2 -3
  2. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/README.md +1 -2
  3. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/pyproject.toml +1 -1
  4. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/main.py +13 -20
  5. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/oobe.py +7 -3
  6. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/self_cli.py +2 -1
  7. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/config/__init__.py +8 -4
  8. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/install.py +22 -20
  9. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/telemetry/aggregate.py +0 -5
  10. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/telemetry/provider.py +87 -229
  11. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/telemetry/store.py +15 -68
  12. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/telemetry/telemetry_cli.py +5 -23
  13. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/version.py +1 -1
  14. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/LICENSE-Apache.txt +0 -0
  15. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/__init__.py +0 -0
  16. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/__main__.py +0 -0
  17. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/__init__.py +0 -0
  18. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/builtin_commands.py +0 -0
  19. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/cmd.py +0 -0
  20. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/completer.py +0 -0
  21. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/completion.py +0 -0
  22. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/config_cli.py +0 -0
  23. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/user_input.py +0 -0
  24. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/cli/version_cli.py +0 -0
  25. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/config/editor.py +0 -0
  26. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/config/errors.py +0 -0
  27. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/config/news.py +0 -0
  28. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/config/schema.py +0 -0
  29. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/device/__init__.py +0 -0
  30. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/device/provision.py +0 -0
  31. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/device/provision_cli.py +0 -0
  32. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/log/__init__.py +0 -0
  33. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/mux/__init__.py +0 -0
  34. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/mux/runtime.py +0 -0
  35. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/mux/venv/__init__.py +0 -0
  36. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/mux/venv/emulator_cfg.py +0 -0
  37. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/mux/venv/maker.py +0 -0
  38. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/mux/venv/venv_cli.py +0 -0
  39. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/mux/venv_cfg.py +0 -0
  40. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/pluginhost/__init__.py +0 -0
  41. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/pluginhost/api.py +0 -0
  42. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/pluginhost/ctx.py +0 -0
  43. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/pluginhost/paths.py +0 -0
  44. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/pluginhost/plugin_cli.py +0 -0
  45. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/pluginhost/unsandboxed.py +0 -0
  46. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/py.typed +0 -0
  47. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/resource_bundle/__init__.py +0 -0
  48. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/resource_bundle/__main__.py +0 -0
  49. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/resource_bundle/data.py +0 -0
  50. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/__init__.py +0 -0
  51. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/admin_checksum.py +0 -0
  52. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/admin_cli.py +0 -0
  53. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/atom.py +0 -0
  54. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/augmented_pkg.py +0 -0
  55. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/canonical_dump.py +0 -0
  56. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/checksum.py +0 -0
  57. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/cli_completion.py +0 -0
  58. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/distfile.py +0 -0
  59. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/entity.py +0 -0
  60. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/entity_cli.py +0 -0
  61. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/entity_provider.py +0 -0
  62. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/fetcher.py +0 -0
  63. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/host.py +0 -0
  64. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/install_cli.py +0 -0
  65. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/list.py +0 -0
  66. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/list_cli.py +0 -0
  67. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/list_filter.py +0 -0
  68. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/msg.py +0 -0
  69. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/news.py +0 -0
  70. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/news_cli.py +0 -0
  71. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/news_store.py +0 -0
  72. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/pkg_manifest.py +0 -0
  73. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/profile.py +0 -0
  74. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/profile_cli.py +0 -0
  75. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/protocols.py +0 -0
  76. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/repo.py +0 -0
  77. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/state.py +0 -0
  78. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/unpack.py +0 -0
  79. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/unpack_method.py +0 -0
  80. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/ruyipkg/update_cli.py +0 -0
  81. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/telemetry/__init__.py +0 -0
  82. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/telemetry/event.py +0 -0
  83. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/telemetry/scope.py +0 -0
  84. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/__init__.py +0 -0
  85. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/ar.py +0 -0
  86. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/ci.py +0 -0
  87. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/frontmatter.py +0 -0
  88. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/git.py +0 -0
  89. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/global_mode.py +0 -0
  90. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/l10n.py +0 -0
  91. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/markdown.py +0 -0
  92. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/mounts.py +0 -0
  93. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/node_info.py +0 -0
  94. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/nuitka.py +0 -0
  95. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/porcelain.py +0 -0
  96. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/prereqs.py +0 -0
  97. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/ssl_patch.py +0 -0
  98. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/templating.py +0 -0
  99. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/toml.py +0 -0
  100. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/url.py +0 -0
  101. {ruyi-0.44.0 → ruyi-0.44.0a20251118}/ruyi/utils/xdg_basedir.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ruyi
3
- Version: 0.44.0
3
+ Version: 0.44.0a20251118
4
4
  Summary: Package manager for RuyiSDK
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -408,8 +408,7 @@ described below.
408
408
  There are 3 telemetry modes available:
409
409
 
410
410
  * `local`: data will be collected but not uploaded without user action.
411
- * `off`: data will neither be collected nor uploaded, except for a one-time
412
- upload of `ruyi`'s version number on first run.
411
+ * `off`: data will not be collected nor uploaded.
413
412
  * `on`: data will be collected and periodically uploaded.
414
413
 
415
414
  By default the `local` mode is active from `ruyi` 0.42.0 (inclusive) on, which
@@ -163,8 +163,7 @@ described below.
163
163
  There are 3 telemetry modes available:
164
164
 
165
165
  * `local`: data will be collected but not uploaded without user action.
166
- * `off`: data will neither be collected nor uploaded, except for a one-time
167
- upload of `ruyi`'s version number on first run.
166
+ * `off`: data will not be collected nor uploaded.
168
167
  * `on`: data will be collected and periodically uploaded.
169
168
 
170
169
  By default the `local` mode is active from `ruyi` 0.42.0 (inclusive) on, which
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "ruyi"
7
- version = "0.44.0"
7
+ version = "0.44.0-alpha.20251118"
8
8
  description = "Package manager for RuyiSDK"
9
9
  keywords = ["ruyi", "ruyisdk"]
10
10
  license = { file = "LICENSE-Apache.txt" }
@@ -39,11 +39,11 @@ def main(gm: GlobalModeProvider, gc: GlobalConfig, argv: list[str]) -> int:
39
39
  if not gm.is_cli_autocomplete:
40
40
  oobe = OOBE(gc)
41
41
 
42
- tm = gc.telemetry
43
- tm.check_first_run_status()
44
- tm.init_installation(False)
45
- atexit.register(tm.flush)
46
- oobe.handlers.append(tm.oobe_prompt)
42
+ if tm := gc.telemetry:
43
+ tm.check_first_run_status()
44
+ tm.init_installation(False)
45
+ atexit.register(tm.flush)
46
+ oobe.handlers.append(tm.oobe_prompt)
47
47
 
48
48
  oobe.maybe_prompt()
49
49
 
@@ -59,11 +59,12 @@ def main(gm: GlobalModeProvider, gc: GlobalConfig, argv: list[str]) -> int:
59
59
  from ..mux.runtime import mux_main
60
60
 
61
61
  # record an invocation and the command name being proxied to
62
- gc.telemetry.record(
63
- TelemetryScope(None),
64
- "cli:mux-invocation-v1",
65
- target=os.path.basename(gm.argv0),
66
- )
62
+ if tm := gc.telemetry:
63
+ tm.record(
64
+ TelemetryScope(None),
65
+ "cli:mux-invocation-v1",
66
+ target=os.path.basename(gm.argv0),
67
+ )
67
68
 
68
69
  return mux_main(gm, gc, argv)
69
70
 
@@ -126,16 +127,8 @@ def main(gm: GlobalModeProvider, gc: GlobalConfig, argv: list[str]) -> int:
126
127
  except AttributeError:
127
128
  pass
128
129
 
129
- tm = gc.telemetry
130
- tm.print_telemetry_notice()
131
-
132
- # Do not record `ruyi telemetry --cron-upload` invocations.
133
- skip_recording_invocation = telemetry_key == "telemetry" and getattr(
134
- args,
135
- "cron_upload",
136
- False,
137
- )
138
- if not skip_recording_invocation:
130
+ if tm := gc.telemetry:
131
+ tm.print_telemetry_notice()
139
132
  tm.record(
140
133
  TelemetryScope(None),
141
134
  "cli:invocation-v1",
@@ -30,9 +30,13 @@ class OOBE:
30
30
  ]
31
31
 
32
32
  def is_first_run(self) -> bool:
33
- # We now always have our first-run indicator because of the minimal
34
- # telemetry mode.
35
- return self._gc.telemetry.is_first_run
33
+ if tm := self._gc.telemetry:
34
+ return tm.is_first_run
35
+ # cannot reliably determine first run status without telemetry
36
+ # we may revisit this later if it turns out users want OOBE tips even
37
+ # if they know how to disable telemetry (hence more likely to be power
38
+ # users)
39
+ return False
36
40
 
37
41
  def should_prompt(self) -> bool:
38
42
  from ..utils.global_mode import is_env_var_truthy
@@ -241,7 +241,8 @@ def _do_reset(
241
241
 
242
242
  # do not record any telemetry data if we're purging it
243
243
  if all_state or telemetry:
244
- cfg.telemetry.discard_events(True)
244
+ if tm := cfg.telemetry:
245
+ tm.discard_events(True)
245
246
 
246
247
  if all_state:
247
248
  status("removing state data")
@@ -297,13 +297,17 @@ class GlobalConfig:
297
297
  def telemetry_root(self) -> os.PathLike[Any]:
298
298
  return pathlib.Path(self.ensure_state_dir()) / "telemetry"
299
299
 
300
+ @property
301
+ def telemetry(self) -> "TelemetryProvider | None":
302
+ return None if self.telemetry_mode == "off" else self._telemetry_provider
303
+
300
304
  @cached_property
301
- def telemetry(self) -> "TelemetryProvider":
305
+ def _telemetry_provider(self) -> "TelemetryProvider | None":
306
+ """Do not access directly; use the ``telemetry`` property instead."""
307
+
302
308
  from ..telemetry.provider import TelemetryProvider
303
309
 
304
- # for allowing minimal uploads when telemetry is off
305
- minimal_mode = self.telemetry_mode == "off"
306
- return TelemetryProvider(self, minimal_mode)
310
+ return None if self.telemetry_mode == "off" else TelemetryProvider(self)
307
311
 
308
312
  @property
309
313
  def telemetry_mode(self) -> str:
@@ -168,16 +168,17 @@ def do_install_atoms(
168
168
  for s in sv.render_known_issues(pm.repo.messages, config.lang_code):
169
169
  logger.I(s)
170
170
 
171
- config.telemetry.record(
172
- TelemetryScope(mr.repo_id),
173
- "repo:package-install-v1",
174
- atom=a_str,
175
- host=canonicalized_host,
176
- pkg_category=pm.category,
177
- pkg_kinds=pm.kind,
178
- pkg_name=pm.name,
179
- pkg_version=pm.ver,
180
- )
171
+ if tm := config.telemetry:
172
+ tm.record(
173
+ TelemetryScope(mr.repo_id),
174
+ "repo:package-install-v1",
175
+ atom=a_str,
176
+ host=canonicalized_host,
177
+ pkg_category=pm.category,
178
+ pkg_kinds=pm.kind,
179
+ pkg_name=pm.name,
180
+ pkg_version=pm.ver,
181
+ )
181
182
 
182
183
  if pm.binary_metadata is not None:
183
184
  ret = _do_install_binary_pkg(
@@ -477,16 +478,17 @@ def do_uninstall_atoms(
477
478
  for a_str, pm in pms_to_uninstall:
478
479
  pkg_name = pm.name_for_installation
479
480
 
480
- config.telemetry.record(
481
- TelemetryScope(mr.repo_id),
482
- "repo:package-uninstall-v1",
483
- atom=a_str,
484
- host=canonicalized_host,
485
- pkg_category=pm.category,
486
- pkg_kinds=pm.kind,
487
- pkg_name=pm.name,
488
- pkg_version=pm.ver,
489
- )
481
+ if tm := config.telemetry:
482
+ tm.record(
483
+ TelemetryScope(mr.repo_id),
484
+ "repo:package-uninstall-v1",
485
+ atom=a_str,
486
+ host=canonicalized_host,
487
+ pkg_category=pm.category,
488
+ pkg_kinds=pm.kind,
489
+ pkg_name=pm.name,
490
+ pkg_version=pm.ver,
491
+ )
490
492
 
491
493
  if pm.binary_metadata is not None:
492
494
  ret = _do_uninstall_binary_pkg(
@@ -18,13 +18,8 @@ class UploadPayload(TypedDict):
18
18
  fmt: int
19
19
  nonce: str
20
20
  ruyi_version: str
21
- report_uuid: "NotRequired[str]"
22
- """Optional field in case the client wishes to report this, and nothing
23
- else. If `installation` is present, this field is ignored."""
24
21
  installation: "NotRequired[NodeInfo | None]"
25
- """More detailed installation info that the client has user consent to report."""
26
22
  events: list[AggregatedTelemetryEvent]
27
- """Aggregated telemetry events that the client has user consent to upload."""
28
23
 
29
24
 
30
25
  def stringify_param_val(v: object) -> str:
@@ -19,27 +19,22 @@ if TYPE_CHECKING:
19
19
  FALLBACK_PM_TELEMETRY_ENDPOINT = "https://api.ruyisdk.cn/telemetry/pm/"
20
20
 
21
21
  TELEMETRY_CONSENT_AND_UPLOAD_DESC = """
22
- RuyiSDK collects minimal usage data in the form of just a version number of
23
- the running [yellow]ruyi[/], to help us improve the product. With your consent,
24
- RuyiSDK may also collect additional non-tracking usage data to be sent
25
- periodically. The data will be recorded and processed by RuyiSDK team-managed
26
- servers located in the Chinese mainland.
22
+ RuyiSDK collects anonymized usage data locally to help us improve the product.
27
23
 
28
24
  [green]By default, nothing leaves your machine[/], and you can also turn off usage data
29
- collection completely. Only with your explicit permission can [yellow]ruyi[/] collect and
30
- upload more usage data. You can change this setting at any time by running
25
+ collection completely. Only with your explicit permission can [yellow]ruyi[/] upload
26
+ collected telemetry, periodically to RuyiSDK team-managed servers located in
27
+ the Chinese mainland. You can change this setting at any time by running
31
28
  [yellow]ruyi telemetry consent[/], [yellow]ruyi telemetry local[/], or [yellow]ruyi telemetry optout[/].
32
29
 
33
- We'll also send a one-time report from this [yellow]ruyi[/] installation so the RuyiSDK
34
- team can better understand adoption. If you choose to opt out, this will be the
35
- only data to be ever uploaded, without any tracking ID being generated or kept.
36
- Thank you for helping us build a better experience!
30
+ If you enable uploads now, we'll also send a one-time report from this [yellow]ruyi[/]
31
+ installation so the RuyiSDK team can better understand adoption. Thank you for
32
+ helping us build a better experience!
37
33
  """
38
34
  TELEMETRY_CONSENT_AND_UPLOAD_PROMPT = (
39
- "Do you agree to have usage data periodically uploaded?"
35
+ "Enable telemetry uploads and send a one-time report now?"
40
36
  )
41
- TELEMETRY_OPTOUT_PROMPT = "\nDo you want to opt out of telemetry entirely?"
42
- MALFORMED_TELEMETRY_STATE_MSG = "malformed telemetry state: unable to determine upload weekday, nothing will be uploaded"
37
+ TELEMETRY_OPTOUT_PROMPT = "\nDo you want to disable telemetry entirely?"
43
38
 
44
39
 
45
40
  def next_utc_weekday(wday: int, now: float | None = None) -> int:
@@ -135,7 +130,7 @@ def set_telemetry_mode(
135
130
 
136
131
 
137
132
  class TelemetryProvider:
138
- def __init__(self, gc: "GlobalConfig", minimal: bool) -> None:
133
+ def __init__(self, gc: "GlobalConfig") -> None:
139
134
  self.state_root = pathlib.Path(gc.telemetry_root)
140
135
 
141
136
  self._discard_events = False
@@ -143,7 +138,6 @@ class TelemetryProvider:
143
138
  self._is_first_run = False
144
139
  self._stores: dict[TelemetryScope, TelemetryStore] = {}
145
140
  self._upload_on_exit = False
146
- self.minimal = minimal
147
141
 
148
142
  # create the PM store
149
143
  self.init_store(TelemetryScope(None))
@@ -160,8 +154,6 @@ class TelemetryProvider:
160
154
 
161
155
  @property
162
156
  def upload_consent_time(self) -> datetime.datetime | None:
163
- if self.minimal or self.local_mode:
164
- return None
165
157
  return self._gc.telemetry_upload_consent_time
166
158
 
167
159
  def store(self, scope: TelemetryScope) -> TelemetryStore | None:
@@ -211,18 +203,11 @@ class TelemetryProvider:
211
203
  def installation_file(self) -> pathlib.Path:
212
204
  return self.state_root / "installation.json"
213
205
 
214
- @property
215
- def minimal_installation_marker_file(self) -> pathlib.Path:
216
- return self.state_root / "minimal-installation-marker"
217
-
218
206
  def check_first_run_status(self) -> None:
219
207
  """Check if this is the first run of the application by checking if installation file exists.
220
208
  This must be done before init_installation() is potentially called.
221
209
  """
222
- self._is_first_run = (
223
- not self.installation_file.exists()
224
- and not self.minimal_installation_marker_file.exists()
225
- )
210
+ self._is_first_run = not self.installation_file.exists()
226
211
 
227
212
  @property
228
213
  def is_first_run(self) -> bool:
@@ -230,15 +215,9 @@ class TelemetryProvider:
230
215
  return self._is_first_run
231
216
 
232
217
  def init_installation(self, force_reinit: bool) -> NodeInfo | None:
233
- if self.minimal:
234
- # be extra safe by not reading or writing installation data at all
235
- # in minimal mode
236
- self._init_minimal_installation_marker(force_reinit)
237
- return None
238
-
239
218
  installation_file = self.installation_file
240
219
  if installation_file.exists() and not force_reinit:
241
- return self._read_installation_data()
220
+ return self.read_installation_data()
242
221
 
243
222
  # either this is a fresh installation or we're forcing a refresh
244
223
  installation_id = uuid.uuid4()
@@ -253,26 +232,13 @@ class TelemetryProvider:
253
232
  fp.write(json.dumps(installation_data).encode("utf-8"))
254
233
  return installation_data
255
234
 
256
- def _init_minimal_installation_marker(self, force_reinit: bool) -> None:
257
- if self.minimal_installation_marker_file.exists() and not force_reinit:
258
- return
259
-
260
- self.logger.D("initializing minimal installation marker file")
261
- self.state_root.mkdir(parents=True, exist_ok=True)
262
-
263
- # just touch the file
264
- self.minimal_installation_marker_file.touch()
265
-
266
- def _read_installation_data(self) -> NodeInfo | None:
235
+ def read_installation_data(self) -> NodeInfo | None:
267
236
  with open(self.installation_file, "rb") as fp:
268
237
  return cast(NodeInfo, json.load(fp))
269
238
 
270
- def _upload_weekday(self) -> int | None:
271
- if self.minimal:
272
- return None
273
-
239
+ def upload_weekday(self) -> int | None:
274
240
  try:
275
- installation_data = self._read_installation_data()
241
+ installation_data = self.read_installation_data()
276
242
  except FileNotFoundError:
277
243
  # init the node info if it's gone
278
244
  installation_data = self.init_installation(False)
@@ -287,29 +253,55 @@ class TelemetryProvider:
287
253
 
288
254
  return report_uuid_prefix % 7 # 0 is Monday
289
255
 
290
- def _has_upload_consent(self, time_now: float | None = None) -> bool:
256
+ def has_upload_consent(self, time_now: float | None = None) -> bool:
291
257
  if self.upload_consent_time is None:
292
258
  return False
293
259
  if time_now is None:
294
260
  time_now = time.time()
295
261
  return self.upload_consent_time.timestamp() <= time_now
296
262
 
297
- def _print_upload_schedule_notice(self, upload_wday: int, now: float) -> None:
263
+ def print_telemetry_notice(self, for_cli_verbose_output: bool = False) -> None:
264
+ if self.local_mode:
265
+ if for_cli_verbose_output:
266
+ self.logger.I(
267
+ "telemetry mode is [green]local[/]: local data collection only, no uploads"
268
+ )
269
+ return
270
+
271
+ now = time.time()
272
+ if self.has_upload_consent(now) and not for_cli_verbose_output:
273
+ self.logger.D("user has consented to telemetry upload")
274
+ return
275
+
276
+ upload_wday = self.upload_weekday()
277
+ if upload_wday is None:
278
+ return
279
+ upload_wday_name = calendar.day_name[upload_wday]
280
+
298
281
  next_upload_day_ts = next_utc_weekday(upload_wday, now)
299
282
  next_upload_day = time.localtime(next_upload_day_ts)
300
283
  next_upload_day_end = time.localtime(next_upload_day_ts + 86400)
301
284
  next_upload_day_str = time.strftime("%Y-%m-%d %H:%M:%S %z", next_upload_day)
302
285
  next_upload_day_end_str = time.strftime(
303
- "%Y-%m-%d %H:%M:%S %z",
304
- next_upload_day_end,
286
+ "%Y-%m-%d %H:%M:%S %z", next_upload_day_end
305
287
  )
306
288
 
307
- if self._is_upload_day(now):
289
+ today_is_upload_day = self.is_upload_day(now)
290
+ if for_cli_verbose_output:
291
+ self.logger.I(
292
+ "telemetry mode is [green]on[/]: data is collected and periodically uploaded"
293
+ )
294
+ self.logger.I(
295
+ f"non-tracking usage information will be uploaded to RuyiSDK-managed servers [bold green]every {upload_wday_name}[/]"
296
+ )
297
+ else:
298
+ self.logger.W(
299
+ f"this [yellow]ruyi[/] installation has telemetry mode set to [yellow]on[/], and [bold]will upload non-tracking usage information to RuyiSDK-managed servers[/] [bold green]every {upload_wday_name}[/]"
300
+ )
301
+
302
+ if today_is_upload_day:
308
303
  for scope, store in self._stores.items():
309
- has_uploaded_today = self._has_uploaded_today(
310
- store.last_upload_timestamp,
311
- now,
312
- )
304
+ has_uploaded_today = self.has_uploaded_today(scope, now)
313
305
  if has_uploaded_today:
314
306
  if last_upload_time := store.last_upload_timestamp:
315
307
  last_upload_time_str = time.strftime(
@@ -328,165 +320,53 @@ class TelemetryProvider:
328
320
  )
329
321
  else:
330
322
  self.logger.I(
331
- "the next upload will happen anytime [yellow]ruyi[/] is executed:"
323
+ f"the next upload will happen anytime [yellow]ruyi[/] is executed between [bold green]{next_upload_day_str}[/] and [bold green]{next_upload_day_end_str}[/]"
332
324
  )
333
- self.logger.I(
334
- f" - between [bold green]{next_upload_day_str}[/] and [bold green]{next_upload_day_end_str}[/]"
335
- )
336
- self.logger.I(" - or if the last upload is more than a week ago")
337
-
338
- def print_telemetry_notice(self, for_cli_verbose_output: bool = False) -> None:
339
- if self.minimal:
340
- if for_cli_verbose_output:
341
- self.logger.I(
342
- "telemetry mode is [green]off[/]: nothing is collected or uploaded after the first run"
343
- )
344
- return
345
-
346
- now = time.time()
347
- upload_wday = self._upload_weekday()
348
- if upload_wday is None:
349
- if for_cli_verbose_output:
350
- self.logger.W(MALFORMED_TELEMETRY_STATE_MSG)
351
- else:
352
- self.logger.D(MALFORMED_TELEMETRY_STATE_MSG)
353
- return
354
-
355
- upload_wday_name = calendar.day_name[upload_wday]
356
-
357
- if self.local_mode:
358
- if for_cli_verbose_output:
359
- self.logger.I(
360
- "telemetry mode is [green]local[/]: local usage collection only, no usage uploads except if requested"
361
- )
362
- return
363
-
364
- if self._has_upload_consent(now) and not for_cli_verbose_output:
365
- self.logger.D("user has consented to telemetry upload")
366
- return
367
-
368
- if for_cli_verbose_output:
369
- self.logger.I(
370
- "telemetry mode is [green]on[/]: usage data is collected and periodically uploaded"
371
- )
372
- self.logger.I(
373
- f"non-tracking usage information will be uploaded to RuyiSDK-managed servers [bold green]every {upload_wday_name}[/]"
374
- )
375
- else:
376
- self.logger.W(
377
- f"this [yellow]ruyi[/] installation has telemetry mode set to [yellow]on[/], and [bold]will upload non-tracking usage information to RuyiSDK-managed servers[/] [bold green]every {upload_wday_name}[/]"
378
- )
379
-
380
- self._print_upload_schedule_notice(upload_wday, now)
381
325
 
382
326
  if not for_cli_verbose_output:
383
327
  self.logger.I("in order to hide this banner:")
384
328
  self.logger.I("- opt out with [yellow]ruyi telemetry optout[/]")
385
329
  self.logger.I("- or give consent with [yellow]ruyi telemetry consent[/]")
386
330
 
387
- def _next_upload_day(self, time_now: float | None = None) -> int | None:
388
- upload_wday = self._upload_weekday()
331
+ def next_upload_day(self, time_now: float | None = None) -> int | None:
332
+ upload_wday = self.upload_weekday()
389
333
  if upload_wday is None:
390
334
  return None
391
335
  return next_utc_weekday(upload_wday, time_now)
392
336
 
393
- def _is_upload_day(self, time_now: float | None = None) -> bool:
337
+ def is_upload_day(self, time_now: float | None = None) -> bool:
394
338
  if time_now is None:
395
339
  time_now = time.time()
396
- if upload_day := self._next_upload_day(time_now):
340
+ if upload_day := self.next_upload_day(time_now):
397
341
  return upload_day <= time_now
398
342
  return False
399
343
 
400
- def _has_uploaded_today(
344
+ def has_uploaded_today(
401
345
  self,
402
- last_upload_time: float | None,
346
+ scope: TelemetryScope,
403
347
  time_now: float | None = None,
404
348
  ) -> bool:
405
349
  if time_now is None:
406
350
  time_now = time.time()
407
- if upload_day := self._next_upload_day(time_now):
351
+ if upload_day := self.next_upload_day(time_now):
408
352
  upload_day_end = upload_day + 86400
409
- if last_upload_time is not None:
353
+ store = self.store(scope)
354
+ if store is None:
355
+ return False
356
+ if last_upload_time := store.last_upload_timestamp:
410
357
  return upload_day <= last_upload_time < upload_day_end
411
358
  return False
412
359
 
413
360
  def record(self, scope: TelemetryScope, kind: str, **params: object) -> None:
414
- if self.minimal:
415
- self.logger.D(
416
- f"minimal telemetry mode enabled, discarding event '{kind}' for scope {scope}"
417
- )
418
- return
419
-
420
361
  if store := self.store(scope):
421
362
  return store.record(kind, **params)
422
- self.logger.D(
423
- f"no telemetry store for scope {scope}, discarding event '{kind}'"
424
- )
363
+ self.logger.D(f"no telemetry store for scope {scope}, discarding event")
425
364
 
426
365
  def discard_events(self, v: bool = True) -> None:
427
366
  self._discard_events = v
428
367
 
429
- def _should_proceed_with_upload(
430
- self,
431
- scope: TelemetryScope,
432
- explicit_request: bool,
433
- cron_mode: bool,
434
- now: float,
435
- ) -> tuple[bool, str]:
436
- # proceed to uploading if forced (explicit requested or _upload_on_exit)
437
- # regardless of schedule
438
- if explicit_request:
439
- return True, "explicit request"
440
- if self._upload_on_exit:
441
- return True, "first-run upload on exit"
442
-
443
- # this is not an explicitly requested upload, so only proceed if today
444
- # is the day, or if the last upload is more than a week ago
445
- #
446
- # the last-upload-more-than-a-week-ago check is to avoid situations
447
- # where the user has not run ruyi for a long time, thus missing
448
- # the scheduled upload day.
449
- #
450
- # cron jobs are a mitigation, but we cannot rely on them either, because:
451
- #
452
- # * ruyi is more likely installed user-locally than system-wide, so
453
- # users may not set up cron jobs for themselves;
454
- # * telemetry data is always recorded per user so system-wide cron jobs
455
- # cannot easily access this data.
456
- last_upload_time: float | None = None
457
- if store := self.store(scope):
458
- last_upload_time = store.last_upload_timestamp
459
-
460
- if not self._is_upload_day(now):
461
- if last_upload_time is not None and now - last_upload_time >= 7 * 86400:
462
- return True, "last upload more than a week ago"
463
- return False, "not upload day"
464
- # now we're sure today is the day
465
-
466
- # if we're in cron mode, proceed as if it's an explicit request;
467
- # otherwise, only proceed if mode is "on" and we haven't uploaded yet today
468
- # for this scope
469
- if cron_mode:
470
- return True, "cron mode upload on upload day"
471
-
472
- if self._gc.telemetry_mode != "on":
473
- return False, "telemetry mode not 'on'"
474
-
475
- if not self._has_uploaded_today(last_upload_time, now):
476
- return True, "upload day, not yet uploaded today"
477
- return False, "upload day, already uploaded today"
478
-
479
- def flush(self, *, upload_now: bool = False, cron_mode: bool = False) -> None:
480
- """
481
- Flush collected telemetry data to persistent store, and upload if needed.
482
-
483
- :param upload_now: Upload data right now regardless of schedule.
484
- :type upload_now: bool
485
- :param cron_mode: Whether this flush is called from a cron job. If true,
486
- non-upload-day uploads will be skipped, otherwise acts just like
487
- explicit uploads via `ruyi telemetry upload`.
488
- :type cron_mode: bool
489
- """
368
+ def flush(self, *, upload_now: bool = False) -> None:
369
+ now = time.time()
490
370
 
491
371
  # We may be self-uninstalling and purging all state data, and in this
492
372
  # case we don't want to record anything (thus re-creating directories).
@@ -494,50 +374,31 @@ class TelemetryProvider:
494
374
  self.logger.D("discarding collected telemetry data")
495
375
  return
496
376
 
497
- now = time.time()
498
-
499
- def should_proceed(scope: TelemetryScope) -> tuple[bool, str]:
500
- return self._should_proceed_with_upload(
501
- scope,
502
- explicit_request=upload_now,
503
- cron_mode=cron_mode,
504
- now=now,
505
- )
506
-
507
- if self.minimal:
508
- if not self._upload_on_exit:
509
- self.logger.D("skipping upload for non-first-run in minimal mode")
510
- return
511
-
512
- for scope, store in self._stores.items():
513
- go_ahead, reason = should_proceed(scope)
514
- self.logger.D(
515
- f"minimal telemetry upload check for scope {scope}: go_ahead={go_ahead}, reason={reason}"
516
- )
517
- if not go_ahead:
518
- continue
519
- store.upload_minimal()
520
- return
377
+ self.logger.D("flushing telemetry to persistent store")
521
378
 
522
379
  for scope, store in self._stores.items():
523
- self.logger.D(f"flushing telemetry to persistent store for scope {scope}")
524
380
  store.persist(now)
525
381
 
526
- go_ahead, reason = should_proceed(scope)
527
- self.logger.D(
528
- f"regular telemetry upload check for scope {scope}: go_ahead={go_ahead}, reason={reason}"
529
- )
530
- if not go_ahead:
382
+ # try to upload if forced (upload_now or _upload_on_exit), or:
383
+ #
384
+ # * we're not in local mode
385
+ # * today is the day
386
+ # * we haven't uploaded today
387
+ if not (upload_now or self._upload_on_exit) and (
388
+ self.local_mode
389
+ or not self.is_upload_day(now)
390
+ or self.has_uploaded_today(scope, now)
391
+ ):
531
392
  continue
532
393
 
533
- self._prepare_data_for_upload(store)
534
- store.upload_staged_payloads()
394
+ self.prepare_data_for_upload(store)
395
+ self.upload_staged_payloads(store)
535
396
 
536
- def _prepare_data_for_upload(self, store: TelemetryStore) -> None:
397
+ def prepare_data_for_upload(self, store: TelemetryStore) -> None:
537
398
  installation_data: NodeInfo | None = None
538
399
  if store.scope.is_pm:
539
400
  try:
540
- installation_data = self._read_installation_data()
401
+ installation_data = self.read_installation_data()
541
402
  except FileNotFoundError:
542
403
  # should not happen due to is_upload_day() initializing it for us
543
404
  # beforehand, but proceed without node info nonetheless
@@ -545,20 +406,16 @@ class TelemetryProvider:
545
406
 
546
407
  return store.prepare_data_for_upload(installation_data)
547
408
 
409
+ def upload_staged_payloads(self, store: TelemetryStore) -> None:
410
+ if self.local_mode:
411
+ return
412
+
413
+ return store.upload_staged_payloads()
414
+
548
415
  def oobe_prompt(self) -> None:
549
416
  """Ask whether the user consents to a first-run telemetry upload, and
550
417
  persist the user's exact telemetry choice."""
551
418
 
552
- if self._gc.is_telemetry_optout:
553
- # user has already explicitly opted out via the environment variable,
554
- # don't bother asking
555
- return
556
-
557
- # We always report installation info on first run, regardless of
558
- # user's telemetry choice. In case the user opts out, only do a one-time
559
- # upload now, and never upload anything again.
560
- self._upload_on_exit = True
561
-
562
419
  from ..cli import user_input
563
420
 
564
421
  self.logger.stdout(TELEMETRY_CONSENT_AND_UPLOAD_DESC)
@@ -584,3 +441,4 @@ class TelemetryProvider:
584
441
 
585
442
  consent_time = datetime.datetime.now().astimezone()
586
443
  set_telemetry_mode(self._gc, "on", consent_time)
444
+ self._upload_on_exit = True
@@ -85,11 +85,6 @@ class TelemetryStore:
85
85
  def record_upload_timestamp(self, time_now: float | None = None) -> None:
86
86
  if time_now is None:
87
87
  time_now = time.time()
88
-
89
- # We may not have store_root existing yet if we're in minimal telemetry
90
- # mode
91
- self.store_root.mkdir(parents=True, exist_ok=True)
92
-
93
88
  f = self.last_upload_marker_file
94
89
  f.touch()
95
90
  os.utime(f, (time_now, time_now))
@@ -127,6 +122,10 @@ class TelemetryStore:
127
122
  f"scope {self.scope}: persisted {len(self._events)} telemetry event(s)"
128
123
  )
129
124
 
125
+ def upload(self, installation_data: NodeInfo | None = None) -> None:
126
+ self.prepare_data_for_upload(installation_data)
127
+ self.upload_staged_payloads()
128
+
130
129
  def read_back_raw_events(self) -> Iterable[TelemetryEvent]:
131
130
  try:
132
131
  for f in self.raw_events_dir.glob("run.*.ndjson"):
@@ -178,47 +177,6 @@ class TelemetryStore:
178
177
 
179
178
  self.purge_raw_events()
180
179
 
181
- def prepare_data_for_minimal_upload(self) -> bytes:
182
- """Prepare a minimal upload payload with no installation data and no events.
183
-
184
- Used when user has not consented to telemetry collection but also not
185
- explicitly opted out, for gaining minimal insight into adoption.
186
- """
187
-
188
- # import ruyi.version here because this package is on the CLI startup
189
- # critical path, and version probing is costly there
190
- from ..version import RUYI_SEMVER
191
-
192
- payload_nonce = uuid.uuid4().hex # for server-side dedup purposes
193
-
194
- # We don't have installation data, and cannot have it initialized
195
- # in this case because absence of installation data means no user
196
- # consent. And making up a persistent installation ID is not a
197
- # choice either, because "installation ID" resembles "advertising
198
- # ID" a lot, which is considered personally identifiable information
199
- # (PII) and not allowed to be collected without user consent.
200
- #
201
- # So, resort to re-using the completely random nonce as the report
202
- # UUID, which does not allow for server-side correlation but at least
203
- # allows for some insight into end-user adoption.
204
- payload: UploadPayload = {
205
- "fmt": 1,
206
- "nonce": payload_nonce,
207
- "ruyi_version": RUYI_SEMVER,
208
- "report_uuid": payload_nonce,
209
- "events": [],
210
- }
211
-
212
- return json.dumps(payload).encode("utf-8")
213
-
214
- def upload_minimal(self) -> None:
215
- if not self.api_url:
216
- return
217
-
218
- p = self.prepare_data_for_minimal_upload()
219
- self.upload_one_staged_payload(p, self.api_url)
220
- self.record_upload_timestamp()
221
-
222
180
  def upload_staged_payloads(self) -> None:
223
181
  if not self.api_url:
224
182
  return
@@ -240,7 +198,7 @@ class TelemetryStore:
240
198
 
241
199
  def upload_one_staged_payload(
242
200
  self,
243
- f: pathlib.Path | bytes,
201
+ f: pathlib.Path,
244
202
  endpoint: str,
245
203
  ) -> None:
246
204
  # import ruyi.version here because this package is on the CLI startup
@@ -248,23 +206,13 @@ class TelemetryStore:
248
206
  from ..version import RUYI_USER_AGENT
249
207
 
250
208
  api_path = urljoin_for_sure(endpoint, "upload-v1")
251
-
252
- if isinstance(f, pathlib.Path):
253
- self._logger.D(
254
- f"scope {self.scope}: about to upload payload {f} to {api_path}"
255
- )
256
- data = f.read_bytes()
257
- else:
258
- self._logger.D(
259
- f"scope {self.scope}: about to upload in-memory payload to {api_path}"
260
- )
261
- data = f
209
+ self._logger.D(f"scope {self.scope}: about to upload payload {f} to {api_path}")
262
210
 
263
211
  import requests
264
212
 
265
213
  resp = requests.post(
266
214
  api_path,
267
- data=data,
215
+ data=f.read_bytes(),
268
216
  headers={"User-Agent": RUYI_USER_AGENT},
269
217
  allow_redirects=True,
270
218
  timeout=5,
@@ -280,12 +228,11 @@ class TelemetryStore:
280
228
  f"scope {self.scope}: telemetry upload ok: status code {resp.status_code}"
281
229
  )
282
230
 
283
- if isinstance(f, pathlib.Path):
284
- # move to completed dir
285
- # TODO: rotation
286
- try:
287
- f.rename(self.uploaded_dir / f.name)
288
- except OSError as e:
289
- self._logger.D(
290
- f"scope {self.scope}: failed to move uploaded payload away: {e}"
291
- )
231
+ # move to completed dir
232
+ # TODO: rotation
233
+ try:
234
+ f.rename(self.uploaded_dir / f.name)
235
+ except OSError as e:
236
+ self._logger.D(
237
+ f"scope {self.scope}: failed to move uploaded payload away: {e}"
238
+ )
@@ -13,34 +13,12 @@ if TYPE_CHECKING:
13
13
  class TelemetryCommand(
14
14
  RootCommand,
15
15
  cmd="telemetry",
16
- has_main=True,
17
16
  has_subcommands=True,
18
17
  help="Manage your telemetry preferences",
19
18
  ):
20
19
  @classmethod
21
20
  def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
22
- # https://github.com/python/cpython/issues/67037 prevents the registration
23
- # of undocumented subcommands, so a preferred usage of
24
- # "ruyi telemetry cron-upload" is not possible right now.
25
- p.add_argument(
26
- "--cron-upload",
27
- action="store_true",
28
- dest="cron_upload",
29
- default=False,
30
- help=argparse.SUPPRESS,
31
- )
32
-
33
- @classmethod
34
- def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
35
- cron_upload: bool = args.cron_upload
36
- if not cron_upload:
37
- args._parser.print_help() # pylint: disable=protected-access
38
- return 0
39
-
40
- # the rest are implementation of "--cron-upload"
41
-
42
- cfg.telemetry.flush(cron_mode=True)
43
- return 0
21
+ pass
44
22
 
45
23
 
46
24
  class TelemetryConsentCommand(
@@ -139,6 +117,10 @@ class TelemetryUploadCommand(
139
117
 
140
118
  @classmethod
141
119
  def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
120
+ if cfg.telemetry is None:
121
+ cfg.logger.W("telemetry is disabled, nothing to upload")
122
+ return 0
123
+
142
124
  cfg.telemetry.flush(upload_now=True)
143
125
  # disable the flush at program exit because we have just done that
144
126
  cfg.telemetry.discard_events()
@@ -1,6 +1,6 @@
1
1
  from typing import Final
2
2
 
3
- RUYI_SEMVER: Final = "0.44.0"
3
+ RUYI_SEMVER: Final = "0.44.0-alpha.20251118"
4
4
  RUYI_USER_AGENT: Final = f"ruyi/{RUYI_SEMVER}"
5
5
 
6
6
  COPYRIGHT_NOTICE: Final = """\
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes