ruyi 0.44.0__py3-none-any.whl → 0.44.0a20251118__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.
- ruyi/cli/main.py +13 -20
- ruyi/cli/oobe.py +7 -3
- ruyi/cli/self_cli.py +2 -1
- ruyi/config/__init__.py +8 -4
- ruyi/ruyipkg/install.py +22 -20
- ruyi/telemetry/aggregate.py +0 -5
- ruyi/telemetry/provider.py +87 -229
- ruyi/telemetry/store.py +15 -68
- ruyi/telemetry/telemetry_cli.py +5 -23
- ruyi/version.py +1 -1
- {ruyi-0.44.0.dist-info → ruyi-0.44.0a20251118.dist-info}/METADATA +2 -3
- {ruyi-0.44.0.dist-info → ruyi-0.44.0a20251118.dist-info}/RECORD +15 -15
- {ruyi-0.44.0.dist-info → ruyi-0.44.0a20251118.dist-info}/WHEEL +0 -0
- {ruyi-0.44.0.dist-info → ruyi-0.44.0a20251118.dist-info}/entry_points.txt +0 -0
- {ruyi-0.44.0.dist-info → ruyi-0.44.0a20251118.dist-info}/licenses/LICENSE-Apache.txt +0 -0
ruyi/cli/main.py
CHANGED
|
@@ -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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
130
|
-
|
|
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",
|
ruyi/cli/oobe.py
CHANGED
|
@@ -30,9 +30,13 @@ class OOBE:
|
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
def is_first_run(self) -> bool:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
ruyi/cli/self_cli.py
CHANGED
|
@@ -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
|
|
244
|
+
if tm := cfg.telemetry:
|
|
245
|
+
tm.discard_events(True)
|
|
245
246
|
|
|
246
247
|
if all_state:
|
|
247
248
|
status("removing state data")
|
ruyi/config/__init__.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
ruyi/ruyipkg/install.py
CHANGED
|
@@ -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
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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(
|
ruyi/telemetry/aggregate.py
CHANGED
|
@@ -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:
|
ruyi/telemetry/provider.py
CHANGED
|
@@ -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
|
|
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[/]
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
team can better understand adoption.
|
|
35
|
-
|
|
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
|
-
"
|
|
35
|
+
"Enable telemetry uploads and send a one-time report now?"
|
|
40
36
|
)
|
|
41
|
-
TELEMETRY_OPTOUT_PROMPT = "\nDo you want to
|
|
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"
|
|
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.
|
|
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
|
|
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
|
|
271
|
-
if self.minimal:
|
|
272
|
-
return None
|
|
273
|
-
|
|
239
|
+
def upload_weekday(self) -> int | None:
|
|
274
240
|
try:
|
|
275
|
-
installation_data = self.
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
388
|
-
upload_wday = self.
|
|
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
|
|
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.
|
|
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
|
|
344
|
+
def has_uploaded_today(
|
|
401
345
|
self,
|
|
402
|
-
|
|
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.
|
|
351
|
+
if upload_day := self.next_upload_day(time_now):
|
|
408
352
|
upload_day_end = upload_day + 86400
|
|
409
|
-
|
|
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
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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.
|
|
534
|
-
|
|
394
|
+
self.prepare_data_for_upload(store)
|
|
395
|
+
self.upload_staged_payloads(store)
|
|
535
396
|
|
|
536
|
-
def
|
|
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.
|
|
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
|
ruyi/telemetry/store.py
CHANGED
|
@@ -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
|
|
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=
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
self.
|
|
290
|
-
|
|
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
|
+
)
|
ruyi/telemetry/telemetry_cli.py
CHANGED
|
@@ -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
|
-
|
|
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()
|
ruyi/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ruyi
|
|
3
|
-
Version: 0.44.
|
|
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
|
|
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
|
|
@@ -6,12 +6,12 @@ ruyi/cli/cmd.py,sha256=kR3aEiDE3AfPoP0Zr7MO-09CKoExbkLLmPvve9oKaUg,6725
|
|
|
6
6
|
ruyi/cli/completer.py,sha256=cnOkU7veDe-jP8ROXZL2uBop2HgWfaAZl6dromnPLx8,1426
|
|
7
7
|
ruyi/cli/completion.py,sha256=ffLs3Dv7pY_uinwH98wkBPohRvAjpUOGqy01OTA_Bgo,841
|
|
8
8
|
ruyi/cli/config_cli.py,sha256=9kq5W3Ir_lfwImhvrUmQ1KTKy1aRCv_UU1CmL5eGyJs,4038
|
|
9
|
-
ruyi/cli/main.py,sha256=
|
|
10
|
-
ruyi/cli/oobe.py,sha256=
|
|
11
|
-
ruyi/cli/self_cli.py,sha256=
|
|
9
|
+
ruyi/cli/main.py,sha256=qWgCKbWG8sHxM7rInkoQfzZOHKo2k8JwdgJp_AMH388,4455
|
|
10
|
+
ruyi/cli/oobe.py,sha256=TqXpoCAslIYt1ODPGial0NsNtf3nL1BLxJ8k_CdkuAU,2708
|
|
11
|
+
ruyi/cli/self_cli.py,sha256=UEX3Xjy8RGGn9W2O5cM-5rLCwpsM7uSQ9vYYBYxyjyQ,8837
|
|
12
12
|
ruyi/cli/user_input.py,sha256=ZJPyCAD7Aizkt37f_KDtW683aKvGZ2pL85Rg_y1LLfg,3711
|
|
13
13
|
ruyi/cli/version_cli.py,sha256=L2pejZ7LIPYLUTb9NIz4t51KB8ai8igPBuE64tyqUuI,1275
|
|
14
|
-
ruyi/config/__init__.py,sha256=
|
|
14
|
+
ruyi/config/__init__.py,sha256=9w1dtgvNxIW65eWzhRN4gl5-460hv-GipIoRZ_oqfXc,16859
|
|
15
15
|
ruyi/config/editor.py,sha256=piAJ-a68iX6IFWXy1g9OXoSs_3DardZwoaSPdStKKL0,4243
|
|
16
16
|
ruyi/config/errors.py,sha256=Yrzd3hX5Gb_bCawoVrWjFrLrpO-q_IOtg4ikE0P8ia0,2561
|
|
17
17
|
ruyi/config/news.py,sha256=83LjQjJHsqOPdRrytG7VBFubG6pyDwJ-Mg37gpBRU20,1061
|
|
@@ -51,7 +51,7 @@ ruyi/ruyipkg/entity_cli.py,sha256=hF66i3sJX8XVLIWq376GA9vzgegfQy-6s0x0L831Irk,38
|
|
|
51
51
|
ruyi/ruyipkg/entity_provider.py,sha256=jDfS2Jh01PVHo0kb1XyI5WkZgL4fv21AooXLLwqK-1I,8741
|
|
52
52
|
ruyi/ruyipkg/fetcher.py,sha256=_btz2hkTz0uEUCCSAhOK7tlhAuvzCu42ZTUCP-RpU7U,9047
|
|
53
53
|
ruyi/ruyipkg/host.py,sha256=pmqgggi7koDCWgzFexwHpycv4SZ07VF6xUbi4s8FSKA,1399
|
|
54
|
-
ruyi/ruyipkg/install.py,sha256=
|
|
54
|
+
ruyi/ruyipkg/install.py,sha256=RhJwIqGxMveWrGmpP-2uWpZBRnqkA75HwyHwrNTdqow,18176
|
|
55
55
|
ruyi/ruyipkg/install_cli.py,sha256=joIV5iY4iblDinoH2pgbHNYkMVWqMxtVNj89T0IpJuk,5267
|
|
56
56
|
ruyi/ruyipkg/list.py,sha256=iO7666xFEWKTDNtJqVLaaQKsp0BfLTzgXBwFFHrqCGc,4172
|
|
57
57
|
ruyi/ruyipkg/list_cli.py,sha256=9tRWWRcJv3yJqzup6Oc89E3xahGp5jIfybfEvwwd55I,2243
|
|
@@ -70,12 +70,12 @@ ruyi/ruyipkg/unpack.py,sha256=hPd-lOnDXp1lEb4mdbLUEckT4QXblSFxz-dGAdwaAjc,11207
|
|
|
70
70
|
ruyi/ruyipkg/unpack_method.py,sha256=MonFFvcDb7MVsi2w4yitnCeZkmWmS7nRMMY-wSt9AMs,2106
|
|
71
71
|
ruyi/ruyipkg/update_cli.py,sha256=ywsAAUPctJ3_2qPDvFL2__ql9m8tGZ6ZfrwbSk30Xh4,1425
|
|
72
72
|
ruyi/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
|
-
ruyi/telemetry/aggregate.py,sha256=
|
|
73
|
+
ruyi/telemetry/aggregate.py,sha256=iKFC4re8IA69x8S-wXIGf5doN1C4zyNuyJulSI0xBTs,1988
|
|
74
74
|
ruyi/telemetry/event.py,sha256=GZnFj6E59Q7mjp-2VRApAZH3rT_bu4_cWb5QMCPm-Zc,982
|
|
75
|
-
ruyi/telemetry/provider.py,sha256=
|
|
75
|
+
ruyi/telemetry/provider.py,sha256=RTuGDddQ7yrD8wOlNOjyGhKydD7hiRe3f96pL3pOvmI,16493
|
|
76
76
|
ruyi/telemetry/scope.py,sha256=e45VPAvRAqSxrL0ESorN9SCnR_I6Bwi2CMPJDDshJEE,1133
|
|
77
|
-
ruyi/telemetry/store.py,sha256=
|
|
78
|
-
ruyi/telemetry/telemetry_cli.py,sha256=
|
|
77
|
+
ruyi/telemetry/store.py,sha256=O9YMlLL1iqDbW8ltq0aZRX4VBCZEexBbc335DqlPtRM,8017
|
|
78
|
+
ruyi/telemetry/telemetry_cli.py,sha256=PBVMUSE3P6IBKQVMji_bueVenCdbchbOlowySXy0468,3364
|
|
79
79
|
ruyi/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
80
80
|
ruyi/utils/ar.py,sha256=w9wiYYdbInLewr5IRTcP0TiOw2ibVDQEmnV0hHm9WlA,2271
|
|
81
81
|
ruyi/utils/ci.py,sha256=66DBm4ooA7yozDtXCJFd1n2jJXTsEnxPSpkNzLfE28M,2970
|
|
@@ -94,9 +94,9 @@ ruyi/utils/templating.py,sha256=94xBJTkIfDqmUBTc9hnLO54zQoC7hwGWONGF3YbaqHk,966
|
|
|
94
94
|
ruyi/utils/toml.py,sha256=aniIF3SGfR69_s3GWWwlnoKxW4B5IDVY2CM0eUI55_c,3501
|
|
95
95
|
ruyi/utils/url.py,sha256=Wyct6syS4GmZC6mY7SK-YgBWxKl3cOOBXtp9UtvGkto,186
|
|
96
96
|
ruyi/utils/xdg_basedir.py,sha256=RwVH199jPcLVsg5ngR62RaNS5hqnMpkdt31LqkCfa1g,2751
|
|
97
|
-
ruyi/version.py,sha256=
|
|
98
|
-
ruyi-0.44.
|
|
99
|
-
ruyi-0.44.
|
|
100
|
-
ruyi-0.44.
|
|
101
|
-
ruyi-0.44.
|
|
102
|
-
ruyi-0.44.
|
|
97
|
+
ruyi/version.py,sha256=jT9EYtNy62PkDUnqmR1ruxXf6GwVW0MN9QJjtxtw8RU,610
|
|
98
|
+
ruyi-0.44.0a20251118.dist-info/METADATA,sha256=w6xXJQhK6X8tSfmFTCNX01eTVwosr6Ez2Kmf-p5czRw,24383
|
|
99
|
+
ruyi-0.44.0a20251118.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
100
|
+
ruyi-0.44.0a20251118.dist-info/entry_points.txt,sha256=GXSNSy7OgFrnlU5xm5dE3l3PGO92Qf6VDIUCdvQNm8E,49
|
|
101
|
+
ruyi-0.44.0a20251118.dist-info/licenses/LICENSE-Apache.txt,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
102
|
+
ruyi-0.44.0a20251118.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|