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.
- ruyi/__init__.py +21 -0
- ruyi/__main__.py +98 -0
- ruyi/cli/__init__.py +5 -0
- ruyi/cli/builtin_commands.py +14 -0
- ruyi/cli/cmd.py +224 -0
- ruyi/cli/completer.py +50 -0
- ruyi/cli/completion.py +26 -0
- ruyi/cli/config_cli.py +153 -0
- ruyi/cli/main.py +111 -0
- ruyi/cli/self_cli.py +295 -0
- ruyi/cli/user_input.py +127 -0
- ruyi/cli/version_cli.py +45 -0
- ruyi/config/__init__.py +401 -0
- ruyi/config/editor.py +92 -0
- ruyi/config/errors.py +76 -0
- ruyi/config/news.py +39 -0
- ruyi/config/schema.py +197 -0
- ruyi/device/__init__.py +0 -0
- ruyi/device/provision.py +591 -0
- ruyi/device/provision_cli.py +40 -0
- ruyi/log/__init__.py +272 -0
- ruyi/mux/.gitignore +1 -0
- ruyi/mux/__init__.py +0 -0
- ruyi/mux/runtime.py +213 -0
- ruyi/mux/venv/__init__.py +12 -0
- ruyi/mux/venv/emulator_cfg.py +41 -0
- ruyi/mux/venv/maker.py +782 -0
- ruyi/mux/venv/venv_cli.py +92 -0
- ruyi/mux/venv_cfg.py +214 -0
- ruyi/pluginhost/__init__.py +0 -0
- ruyi/pluginhost/api.py +206 -0
- ruyi/pluginhost/ctx.py +222 -0
- ruyi/pluginhost/paths.py +135 -0
- ruyi/pluginhost/plugin_cli.py +37 -0
- ruyi/pluginhost/unsandboxed.py +246 -0
- ruyi/py.typed +0 -0
- ruyi/resource_bundle/__init__.py +20 -0
- ruyi/resource_bundle/__main__.py +55 -0
- ruyi/resource_bundle/data.py +26 -0
- ruyi/ruyipkg/__init__.py +0 -0
- ruyi/ruyipkg/admin_checksum.py +88 -0
- ruyi/ruyipkg/admin_cli.py +83 -0
- ruyi/ruyipkg/atom.py +184 -0
- ruyi/ruyipkg/augmented_pkg.py +212 -0
- ruyi/ruyipkg/canonical_dump.py +320 -0
- ruyi/ruyipkg/checksum.py +39 -0
- ruyi/ruyipkg/cli_completion.py +42 -0
- ruyi/ruyipkg/distfile.py +208 -0
- ruyi/ruyipkg/entity.py +387 -0
- ruyi/ruyipkg/entity_cli.py +123 -0
- ruyi/ruyipkg/entity_provider.py +273 -0
- ruyi/ruyipkg/fetch.py +271 -0
- ruyi/ruyipkg/host.py +55 -0
- ruyi/ruyipkg/install.py +554 -0
- ruyi/ruyipkg/install_cli.py +150 -0
- ruyi/ruyipkg/list.py +126 -0
- ruyi/ruyipkg/list_cli.py +79 -0
- ruyi/ruyipkg/list_filter.py +173 -0
- ruyi/ruyipkg/msg.py +99 -0
- ruyi/ruyipkg/news.py +123 -0
- ruyi/ruyipkg/news_cli.py +78 -0
- ruyi/ruyipkg/news_store.py +183 -0
- ruyi/ruyipkg/pkg_manifest.py +657 -0
- ruyi/ruyipkg/profile.py +208 -0
- ruyi/ruyipkg/profile_cli.py +33 -0
- ruyi/ruyipkg/protocols.py +55 -0
- ruyi/ruyipkg/repo.py +763 -0
- ruyi/ruyipkg/state.py +345 -0
- ruyi/ruyipkg/unpack.py +369 -0
- ruyi/ruyipkg/unpack_method.py +91 -0
- ruyi/ruyipkg/update_cli.py +54 -0
- ruyi/telemetry/__init__.py +0 -0
- ruyi/telemetry/aggregate.py +72 -0
- ruyi/telemetry/event.py +41 -0
- ruyi/telemetry/node_info.py +192 -0
- ruyi/telemetry/provider.py +411 -0
- ruyi/telemetry/scope.py +43 -0
- ruyi/telemetry/store.py +238 -0
- ruyi/telemetry/telemetry_cli.py +127 -0
- ruyi/utils/__init__.py +0 -0
- ruyi/utils/ar.py +74 -0
- ruyi/utils/ci.py +63 -0
- ruyi/utils/frontmatter.py +38 -0
- ruyi/utils/git.py +169 -0
- ruyi/utils/global_mode.py +204 -0
- ruyi/utils/l10n.py +83 -0
- ruyi/utils/markdown.py +73 -0
- ruyi/utils/nuitka.py +33 -0
- ruyi/utils/porcelain.py +51 -0
- ruyi/utils/prereqs.py +77 -0
- ruyi/utils/ssl_patch.py +170 -0
- ruyi/utils/templating.py +34 -0
- ruyi/utils/toml.py +115 -0
- ruyi/utils/url.py +7 -0
- ruyi/utils/xdg_basedir.py +80 -0
- ruyi/version.py +67 -0
- ruyi-0.39.0.dist-info/LICENSE-Apache.txt +201 -0
- ruyi-0.39.0.dist-info/METADATA +403 -0
- ruyi-0.39.0.dist-info/RECORD +101 -0
- ruyi-0.39.0.dist-info/WHEEL +4 -0
- 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
|