ruyi 0.39.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 (101) hide show
  1. ruyi/__init__.py +21 -0
  2. ruyi/__main__.py +98 -0
  3. ruyi/cli/__init__.py +5 -0
  4. ruyi/cli/builtin_commands.py +14 -0
  5. ruyi/cli/cmd.py +224 -0
  6. ruyi/cli/completer.py +50 -0
  7. ruyi/cli/completion.py +26 -0
  8. ruyi/cli/config_cli.py +153 -0
  9. ruyi/cli/main.py +111 -0
  10. ruyi/cli/self_cli.py +295 -0
  11. ruyi/cli/user_input.py +127 -0
  12. ruyi/cli/version_cli.py +45 -0
  13. ruyi/config/__init__.py +401 -0
  14. ruyi/config/editor.py +92 -0
  15. ruyi/config/errors.py +76 -0
  16. ruyi/config/news.py +39 -0
  17. ruyi/config/schema.py +197 -0
  18. ruyi/device/__init__.py +0 -0
  19. ruyi/device/provision.py +591 -0
  20. ruyi/device/provision_cli.py +40 -0
  21. ruyi/log/__init__.py +272 -0
  22. ruyi/mux/.gitignore +1 -0
  23. ruyi/mux/__init__.py +0 -0
  24. ruyi/mux/runtime.py +213 -0
  25. ruyi/mux/venv/__init__.py +12 -0
  26. ruyi/mux/venv/emulator_cfg.py +41 -0
  27. ruyi/mux/venv/maker.py +782 -0
  28. ruyi/mux/venv/venv_cli.py +92 -0
  29. ruyi/mux/venv_cfg.py +214 -0
  30. ruyi/pluginhost/__init__.py +0 -0
  31. ruyi/pluginhost/api.py +206 -0
  32. ruyi/pluginhost/ctx.py +222 -0
  33. ruyi/pluginhost/paths.py +135 -0
  34. ruyi/pluginhost/plugin_cli.py +37 -0
  35. ruyi/pluginhost/unsandboxed.py +246 -0
  36. ruyi/py.typed +0 -0
  37. ruyi/resource_bundle/__init__.py +20 -0
  38. ruyi/resource_bundle/__main__.py +55 -0
  39. ruyi/resource_bundle/data.py +26 -0
  40. ruyi/ruyipkg/__init__.py +0 -0
  41. ruyi/ruyipkg/admin_checksum.py +88 -0
  42. ruyi/ruyipkg/admin_cli.py +83 -0
  43. ruyi/ruyipkg/atom.py +184 -0
  44. ruyi/ruyipkg/augmented_pkg.py +212 -0
  45. ruyi/ruyipkg/canonical_dump.py +320 -0
  46. ruyi/ruyipkg/checksum.py +39 -0
  47. ruyi/ruyipkg/cli_completion.py +42 -0
  48. ruyi/ruyipkg/distfile.py +208 -0
  49. ruyi/ruyipkg/entity.py +387 -0
  50. ruyi/ruyipkg/entity_cli.py +123 -0
  51. ruyi/ruyipkg/entity_provider.py +273 -0
  52. ruyi/ruyipkg/fetch.py +271 -0
  53. ruyi/ruyipkg/host.py +55 -0
  54. ruyi/ruyipkg/install.py +554 -0
  55. ruyi/ruyipkg/install_cli.py +150 -0
  56. ruyi/ruyipkg/list.py +126 -0
  57. ruyi/ruyipkg/list_cli.py +79 -0
  58. ruyi/ruyipkg/list_filter.py +173 -0
  59. ruyi/ruyipkg/msg.py +99 -0
  60. ruyi/ruyipkg/news.py +123 -0
  61. ruyi/ruyipkg/news_cli.py +78 -0
  62. ruyi/ruyipkg/news_store.py +183 -0
  63. ruyi/ruyipkg/pkg_manifest.py +657 -0
  64. ruyi/ruyipkg/profile.py +208 -0
  65. ruyi/ruyipkg/profile_cli.py +33 -0
  66. ruyi/ruyipkg/protocols.py +55 -0
  67. ruyi/ruyipkg/repo.py +763 -0
  68. ruyi/ruyipkg/state.py +345 -0
  69. ruyi/ruyipkg/unpack.py +369 -0
  70. ruyi/ruyipkg/unpack_method.py +91 -0
  71. ruyi/ruyipkg/update_cli.py +54 -0
  72. ruyi/telemetry/__init__.py +0 -0
  73. ruyi/telemetry/aggregate.py +72 -0
  74. ruyi/telemetry/event.py +41 -0
  75. ruyi/telemetry/node_info.py +192 -0
  76. ruyi/telemetry/provider.py +411 -0
  77. ruyi/telemetry/scope.py +43 -0
  78. ruyi/telemetry/store.py +238 -0
  79. ruyi/telemetry/telemetry_cli.py +127 -0
  80. ruyi/utils/__init__.py +0 -0
  81. ruyi/utils/ar.py +74 -0
  82. ruyi/utils/ci.py +63 -0
  83. ruyi/utils/frontmatter.py +38 -0
  84. ruyi/utils/git.py +169 -0
  85. ruyi/utils/global_mode.py +204 -0
  86. ruyi/utils/l10n.py +83 -0
  87. ruyi/utils/markdown.py +73 -0
  88. ruyi/utils/nuitka.py +33 -0
  89. ruyi/utils/porcelain.py +51 -0
  90. ruyi/utils/prereqs.py +77 -0
  91. ruyi/utils/ssl_patch.py +170 -0
  92. ruyi/utils/templating.py +34 -0
  93. ruyi/utils/toml.py +115 -0
  94. ruyi/utils/url.py +7 -0
  95. ruyi/utils/xdg_basedir.py +80 -0
  96. ruyi/version.py +67 -0
  97. ruyi-0.39.0.dist-info/LICENSE-Apache.txt +201 -0
  98. ruyi-0.39.0.dist-info/METADATA +403 -0
  99. ruyi-0.39.0.dist-info/RECORD +101 -0
  100. ruyi-0.39.0.dist-info/WHEEL +4 -0
  101. ruyi-0.39.0.dist-info/entry_points.txt +3 -0
ruyi/ruyipkg/entity.py ADDED
@@ -0,0 +1,387 @@
1
+ from typing import Any, Callable, Iterable, Iterator, Mapping
2
+
3
+ import fastjsonschema
4
+ from fastjsonschema.exceptions import JsonSchemaException
5
+
6
+ from ..log import RuyiLogger
7
+ from .entity_provider import BaseEntity, BaseEntityProvider, EntityValidationError
8
+
9
+
10
+ class EntityStore:
11
+ def __init__(
12
+ self,
13
+ logger: RuyiLogger,
14
+ *providers: BaseEntityProvider,
15
+ ) -> None:
16
+ """Initialize the entity store.
17
+
18
+ Args:
19
+ logger: The logger to use.
20
+ providers: A list of entity providers to use for loading entity data.
21
+ """
22
+ self._logger = logger
23
+ self._providers = providers
24
+
25
+ self._entity_types: set[str] = set()
26
+ """Cache of entity types discovered."""
27
+
28
+ self._entities: dict[str, dict[str, BaseEntity]] = {}
29
+ """Cache of loaded entities by type."""
30
+
31
+ self._schemas: dict[str, object] = {}
32
+ """Cache of loaded schemas."""
33
+
34
+ self._validators: dict[str, Callable[[object], object | None]] = {}
35
+ """Cache of compiled schema validators."""
36
+
37
+ self._loaded = False
38
+ self._discovered = False
39
+
40
+ def _discover_entity_types(self) -> None:
41
+ """Discover all entity types by examining schemas from all providers."""
42
+ if self._discovered:
43
+ return
44
+
45
+ # Collect schemas from all providers
46
+ for provider in self._providers:
47
+ schemas = provider.discover_schemas()
48
+
49
+ # Add new schemas to our cache
50
+ for entity_type, schema in schemas.items():
51
+ if entity_type not in self._schemas:
52
+ self._schemas[entity_type] = schema
53
+ self._entity_types.add(entity_type)
54
+ self._entities[entity_type] = {}
55
+
56
+ self._logger.D(f"discovered entity types from schemas: {self._entity_types}")
57
+ self._discovered = True
58
+
59
+ def _get_validator(self, entity_type: str) -> Callable[[object], object | None]:
60
+ """Get or create a compiled schema validator for the entity type."""
61
+ if entity_type in self._validators:
62
+ return self._validators[entity_type]
63
+
64
+ schema = self._schemas.get(entity_type)
65
+ if not schema:
66
+ self._logger.W(f"no schema found for entity type: {entity_type}")
67
+ # Return a simple validator that accepts anything
68
+ return lambda x: x
69
+
70
+ try:
71
+ validator = fastjsonschema.compile(schema)
72
+ self._validators[entity_type] = validator
73
+ return validator
74
+ except Exception as e:
75
+ self._logger.W(f"failed to compile schema for {entity_type}: {e}")
76
+ # Return a simple validator that accepts anything
77
+ return lambda x: x
78
+
79
+ def _validate_entity(
80
+ self,
81
+ entity_type: str,
82
+ entity_id: str,
83
+ data: Mapping[str, Any],
84
+ ) -> None:
85
+ """Validate an entity against its schema."""
86
+ validator = self._get_validator(entity_type)
87
+
88
+ try:
89
+ validator(data)
90
+ except JsonSchemaException as e:
91
+ raise EntityValidationError(entity_type, entity_id, e) from e
92
+
93
+ def load_all(self, validate: bool = True) -> None:
94
+ """Load all entities from all providers."""
95
+ if self._loaded:
96
+ return
97
+
98
+ # Discover entity types from schemas if not already done
99
+ self._discover_entity_types()
100
+
101
+ # Load entities from all providers
102
+ for provider in self._providers:
103
+ provider_entities = provider.load_entities(list(self._entity_types))
104
+
105
+ # Merge entities from this provider with our cache
106
+ for entity_type, entities_by_id in provider_entities.items():
107
+ for entity_id, entity_data in entities_by_id.items():
108
+ # Validate entity data
109
+ if validate:
110
+ self._validate_entity(entity_type, entity_id, entity_data)
111
+ # Create and store a generic entity
112
+ self._entities[entity_type][entity_id] = BaseEntity(
113
+ entity_type,
114
+ entity_id,
115
+ entity_data,
116
+ )
117
+
118
+ self._loaded = True
119
+
120
+ # Populate reverse references
121
+ # This must happen after the loaded flag is set, because the getter
122
+ # is lazy and will infinitely recurse otherwise.
123
+ for entity_type, entities in self._entities.items():
124
+ for entity_id, entity in entities.items():
125
+ # Collect reverse references
126
+ for ref in entity.related_refs:
127
+ if related_entity := self.get_entity_by_ref(ref):
128
+ related_entity._add_reverse_ref(str(entity))
129
+
130
+ entity_counts = {t: len(entities) for t, entities in self._entities.items()}
131
+ self._logger.D(f"count of loaded entities: {entity_counts}")
132
+
133
+ def get_entity_types(self) -> Iterator[str]:
134
+ """Get all available entity types from the schemas."""
135
+ self._discover_entity_types()
136
+ yield from self._entity_types
137
+
138
+ def get_entity(self, entity_type: str, entity_id: str) -> BaseEntity | None:
139
+ """Get an entity by type and ID."""
140
+ self.load_all()
141
+ return self._entities.get(entity_type, {}).get(entity_id)
142
+
143
+ def iter_entities(
144
+ self,
145
+ entity_type: str | Iterable[str] | None,
146
+ ) -> Iterator[BaseEntity]:
147
+ """Iterate over all entities of a specific type, or all entities."""
148
+ self.load_all()
149
+ if entity_type is not None:
150
+ if isinstance(entity_type, str):
151
+ yield from self._entities.get(entity_type, {}).values()
152
+ return
153
+
154
+ # handle multiple entity types
155
+ for et in entity_type:
156
+ yield from self._entities.get(et, {}).values()
157
+ return
158
+
159
+ for entities in self._entities.values():
160
+ yield from entities.values()
161
+
162
+ def get_entity_by_ref(self, ref: str) -> BaseEntity | None:
163
+ """Resolve an entity reference of the form ``type:id``."""
164
+
165
+ if ":" not in ref:
166
+ raise ValueError(f"Invalid entity reference: {ref}")
167
+ entity_type, entity_id = ref.split(":", 1)
168
+
169
+ self.load_all()
170
+ return self.get_entity(entity_type, entity_id)
171
+
172
+ def list_related_entities(
173
+ self,
174
+ entity: BaseEntity | str,
175
+ reverse_refs: bool = False,
176
+ ) -> list[BaseEntity]:
177
+ """Get all directly related entities of the given entity.
178
+
179
+ Args:
180
+ entity: The entity whose related entities to retrieve, or an entity reference
181
+ in the form ``type:id``.
182
+ reverse_refs: If True, return reverse references instead of forward references.
183
+
184
+ Returns:
185
+ A list of directly related entities
186
+ """
187
+
188
+ if isinstance(entity, str):
189
+ e = self.get_entity_by_ref(entity)
190
+ if e is None:
191
+ raise ValueError(f"Entity not found: {entity}")
192
+ entity = e
193
+
194
+ related_entities = []
195
+ for ref in entity.reverse_refs if reverse_refs else entity.related_refs:
196
+ related_entity = self.get_entity_by_ref(ref)
197
+ if related_entity:
198
+ related_entities.append(related_entity)
199
+ return related_entities
200
+
201
+ def traverse_related_entities(
202
+ self,
203
+ entity: BaseEntity | str,
204
+ transitive: bool = False,
205
+ no_direct_refs: bool = False,
206
+ forward_refs: bool = True,
207
+ reverse_refs: bool = False,
208
+ entity_types: list[str] | None = None,
209
+ ) -> Iterator[BaseEntity]:
210
+ """Traverse related entities of the given entity.
211
+
212
+ Args:
213
+ entity: The starting entity or reference (in the form ``type:id``).
214
+ transitive: If True, traverse the transitive closure of related entities.
215
+ If False, only traverse direct related entities.
216
+ no_direct_refs: If True, skip direct references.
217
+ forward_refs: If True, traverse forward references.
218
+ reverse_refs: If True, traverse reverse references.
219
+ entity_types: Optional list of entity types to filter by. If provided,
220
+ only entities of the specified types will be yielded.
221
+
222
+ Returns:
223
+ An iterator over the related entities
224
+ """
225
+
226
+ if isinstance(entity, str):
227
+ # If a string is provided, resolve it to an entity
228
+ e = self.get_entity_by_ref(entity)
229
+ if e is None:
230
+ raise ValueError(f"Entity not found: {entity}")
231
+ entity = e
232
+
233
+ # Set to track visited entities and avoid cycles
234
+ visited = set()
235
+
236
+ # Helper function for recursive traversal
237
+ def _traverse(
238
+ current_entity: BaseEntity,
239
+ path: list[BaseEntity],
240
+ ) -> Iterator[BaseEntity]:
241
+ # Skip if already visited (prevents cycles)
242
+ if current_entity in visited:
243
+ return
244
+
245
+ # Enforce uniqueness-among-type
246
+ if current_entity.unique_among_type_during_traversal:
247
+ for e in path:
248
+ if e.entity_type == current_entity.entity_type:
249
+ return
250
+
251
+ depth = len(path)
252
+
253
+ # Do not yield related entities if either:
254
+ # - we're the root entity (depth == 0)
255
+ # - no_direct_refs is True and we're at depth == 1
256
+ skip_current_level = depth == 0 or (no_direct_refs and depth == 1)
257
+
258
+ # Check if this entity matches the desired type filter
259
+ entity_type_okay = (
260
+ entity_types is None or current_entity.entity_type in entity_types
261
+ )
262
+
263
+ if not skip_current_level and entity_type_okay:
264
+ yield current_entity
265
+
266
+ # Mark as visited
267
+ visited.add(current_entity)
268
+
269
+ new_path = path.copy()
270
+ new_path.append(current_entity)
271
+
272
+ # Process forward edges if requested
273
+ if forward_refs:
274
+ for related_entity in self.list_related_entities(
275
+ current_entity,
276
+ reverse_refs=False,
277
+ ):
278
+ # Recursively traverse if transitive mode is enabled
279
+ # or if we're at the root entity
280
+ if depth == 0 or transitive:
281
+ yield from _traverse(related_entity, new_path)
282
+
283
+ # Process reverse edges if requested
284
+ if reverse_refs:
285
+ for related_entity in self.list_related_entities(
286
+ current_entity,
287
+ reverse_refs=True,
288
+ ):
289
+ # Recursively traverse if transitive mode is enabled
290
+ # or if we're at the root entity
291
+ if depth == 0 or transitive:
292
+ yield from _traverse(related_entity, new_path)
293
+
294
+ # Start traversal from the given entity
295
+ yield from _traverse(entity, [])
296
+
297
+ def is_entity_related_to(
298
+ self,
299
+ entity: BaseEntity | str,
300
+ related_entity: BaseEntity | str,
301
+ transitive: bool = False,
302
+ unidirectional: bool = True,
303
+ not_found_ok: bool = True,
304
+ ) -> bool:
305
+ """Check if the given entity is related to another entity.
306
+
307
+ Args:
308
+ entity: The starting entity or reference (in the form ``type:id``).
309
+ related_entity: The related entity or reference (in the form ``type:id``).
310
+ transitive: If True, check for transitive relationships.
311
+ unidirectional: If True, entities are considered related if and only if
312
+ the relationship chain consists of forward or reverse
313
+ edges only.
314
+ not_found_ok: If True, return False if either entity is not found.
315
+ If False, raise an error if either entity is not found.
316
+
317
+ Returns:
318
+ True if the entities are related, False otherwise.
319
+ """
320
+
321
+ if isinstance(entity, str):
322
+ e = self.get_entity_by_ref(entity)
323
+ if e is None:
324
+ if not_found_ok:
325
+ return False
326
+ raise ValueError(f"Entity not found: {entity}")
327
+ entity = e
328
+
329
+ if isinstance(related_entity, str):
330
+ re = self.get_entity_by_ref(related_entity)
331
+ if re is None:
332
+ if not_found_ok:
333
+ return False
334
+ raise ValueError(f"Entity not found: {related_entity}")
335
+ related_entity = re
336
+
337
+ # Check if the two entities are directly related
338
+ if related_entity in self.list_related_entities(entity):
339
+ return True
340
+ if related_entity in self.list_related_entities(entity, reverse_refs=True):
341
+ return True
342
+
343
+ # If transitive mode is enabled, check for indirect relationships
344
+ if transitive:
345
+ if unidirectional:
346
+ for e in self.traverse_related_entities(
347
+ entity,
348
+ forward_refs=True,
349
+ reverse_refs=False,
350
+ transitive=True,
351
+ ):
352
+ if related_entity in self.list_related_entities(
353
+ e,
354
+ reverse_refs=False,
355
+ ):
356
+ return True
357
+
358
+ for e in self.traverse_related_entities(
359
+ entity,
360
+ forward_refs=False,
361
+ reverse_refs=True,
362
+ transitive=True,
363
+ ):
364
+ if related_entity in self.list_related_entities(
365
+ e,
366
+ reverse_refs=True,
367
+ ):
368
+ return True
369
+ else:
370
+ for e in self.traverse_related_entities(
371
+ entity,
372
+ forward_refs=True,
373
+ reverse_refs=True,
374
+ transitive=True,
375
+ ):
376
+ if related_entity in self.list_related_entities(
377
+ e,
378
+ reverse_refs=False,
379
+ ):
380
+ return True
381
+ if related_entity in self.list_related_entities(
382
+ e,
383
+ reverse_refs=True,
384
+ ):
385
+ return True
386
+
387
+ return False
@@ -0,0 +1,123 @@
1
+ import argparse
2
+ from typing import TYPE_CHECKING
3
+
4
+ from ..cli.cmd import RootCommand
5
+
6
+ if TYPE_CHECKING:
7
+ from ..cli.completion import ArgumentParser
8
+ from ..config import GlobalConfig
9
+
10
+
11
+ class EntityCommand(
12
+ RootCommand,
13
+ cmd="entity",
14
+ has_subcommands=True,
15
+ is_experimental=True,
16
+ help="Interact with entities defined in the repositories",
17
+ ):
18
+ @classmethod
19
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
20
+ pass
21
+
22
+
23
+ class EntityDescribeCommand(
24
+ EntityCommand,
25
+ cmd="describe",
26
+ help="Describe an entity",
27
+ ):
28
+ @classmethod
29
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
30
+ p.add_argument(
31
+ "ref",
32
+ help="Reference to the entity to describe in the form of '<type>:<name>'",
33
+ )
34
+
35
+ @classmethod
36
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
37
+ logger = cfg.logger
38
+ ref = args.ref
39
+
40
+ entity_store = cfg.repo.entity_store
41
+ entity = entity_store.get_entity_by_ref(ref)
42
+ if entity is None:
43
+ logger.F(f"entity [yellow]{ref}[/] not found")
44
+ return 1
45
+
46
+ logger.stdout(
47
+ f"Entity [bold]{str(entity)}[/] ([green]{entity.display_name}[/])\n"
48
+ )
49
+
50
+ fwd_refs = entity.related_refs
51
+ if fwd_refs:
52
+ logger.stdout(" Direct forward relationships:")
53
+ for ref in sorted(fwd_refs):
54
+ logger.stdout(f" - [yellow]{ref}[/]")
55
+ else:
56
+ logger.stdout(" Direct forward relationships: [gray]none[/]")
57
+
58
+ rev_refs = entity.reverse_refs
59
+ if rev_refs:
60
+ logger.stdout(" Direct reverse relationships:")
61
+ for ref in sorted(rev_refs):
62
+ logger.stdout(f" - [yellow]{ref}[/]")
63
+ else:
64
+ logger.stdout(" Direct reverse relationships: [gray]none[/]")
65
+
66
+ logger.stdout(" All indirectly related entities:")
67
+ for e in entity_store.traverse_related_entities(
68
+ entity,
69
+ transitive=True,
70
+ no_direct_refs=True,
71
+ forward_refs=True,
72
+ reverse_refs=True,
73
+ ):
74
+ logger.stdout(f" - [yellow]{e}[/]")
75
+
76
+ # TODO: render type-specific data
77
+
78
+ return 0
79
+
80
+
81
+ class EntityListCommand(
82
+ EntityCommand,
83
+ cmd="list",
84
+ help="List entities",
85
+ ):
86
+ @classmethod
87
+ def configure_args(cls, gc: "GlobalConfig", p: "ArgumentParser") -> None:
88
+ p.add_argument(
89
+ "-t",
90
+ "--entity-type",
91
+ action="append",
92
+ nargs=1,
93
+ dest="entity_type",
94
+ help="List entities of this type. Can be passed multiple times to list multiple types.",
95
+ )
96
+
97
+ @classmethod
98
+ def main(cls, cfg: "GlobalConfig", args: argparse.Namespace) -> int:
99
+ from ..utils.porcelain import PorcelainOutput
100
+
101
+ entity_types_in: list[list[str]] | None = args.entity_type
102
+ entity_types: list[str] | None = None
103
+ if entity_types_in is not None:
104
+ entity_types = [x[0] for x in entity_types_in]
105
+
106
+ logger = cfg.logger
107
+ entity_store = cfg.repo.entity_store
108
+
109
+ # Check if porcelain output is requested
110
+ if cfg.is_porcelain:
111
+ with PorcelainOutput() as po:
112
+ for e in entity_store.iter_entities(entity_types):
113
+ po.emit(e.to_porcelain())
114
+ return 0
115
+
116
+ # Human-readable output
117
+ for e in entity_store.iter_entities(entity_types):
118
+ logger.stdout(f"'{str(e)}':")
119
+ logger.stdout(f" display name: {e.display_name}")
120
+ logger.stdout(f" data: {e.data}")
121
+ logger.stdout(f" forward_refs: {e.related_refs}")
122
+ logger.stdout(f" reverse_refs: {e.reverse_refs}")
123
+ return 0