d365fo-client 0.1.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.
- d365fo_client/__init__.py +305 -0
- d365fo_client/auth.py +93 -0
- d365fo_client/cli.py +700 -0
- d365fo_client/client.py +1454 -0
- d365fo_client/config.py +304 -0
- d365fo_client/crud.py +200 -0
- d365fo_client/exceptions.py +49 -0
- d365fo_client/labels.py +528 -0
- d365fo_client/main.py +502 -0
- d365fo_client/mcp/__init__.py +16 -0
- d365fo_client/mcp/client_manager.py +276 -0
- d365fo_client/mcp/main.py +98 -0
- d365fo_client/mcp/models.py +371 -0
- d365fo_client/mcp/prompts/__init__.py +43 -0
- d365fo_client/mcp/prompts/action_execution.py +480 -0
- d365fo_client/mcp/prompts/sequence_analysis.py +349 -0
- d365fo_client/mcp/resources/__init__.py +15 -0
- d365fo_client/mcp/resources/database_handler.py +555 -0
- d365fo_client/mcp/resources/entity_handler.py +176 -0
- d365fo_client/mcp/resources/environment_handler.py +132 -0
- d365fo_client/mcp/resources/metadata_handler.py +283 -0
- d365fo_client/mcp/resources/query_handler.py +135 -0
- d365fo_client/mcp/server.py +432 -0
- d365fo_client/mcp/tools/__init__.py +17 -0
- d365fo_client/mcp/tools/connection_tools.py +175 -0
- d365fo_client/mcp/tools/crud_tools.py +579 -0
- d365fo_client/mcp/tools/database_tools.py +813 -0
- d365fo_client/mcp/tools/label_tools.py +189 -0
- d365fo_client/mcp/tools/metadata_tools.py +766 -0
- d365fo_client/mcp/tools/profile_tools.py +706 -0
- d365fo_client/metadata_api.py +793 -0
- d365fo_client/metadata_v2/__init__.py +59 -0
- d365fo_client/metadata_v2/cache_v2.py +1372 -0
- d365fo_client/metadata_v2/database_v2.py +585 -0
- d365fo_client/metadata_v2/global_version_manager.py +573 -0
- d365fo_client/metadata_v2/search_engine_v2.py +423 -0
- d365fo_client/metadata_v2/sync_manager_v2.py +819 -0
- d365fo_client/metadata_v2/version_detector.py +439 -0
- d365fo_client/models.py +862 -0
- d365fo_client/output.py +181 -0
- d365fo_client/profile_manager.py +342 -0
- d365fo_client/profiles.py +178 -0
- d365fo_client/query.py +162 -0
- d365fo_client/session.py +60 -0
- d365fo_client/utils.py +196 -0
- d365fo_client-0.1.0.dist-info/METADATA +1084 -0
- d365fo_client-0.1.0.dist-info/RECORD +51 -0
- d365fo_client-0.1.0.dist-info/WHEEL +5 -0
- d365fo_client-0.1.0.dist-info/entry_points.txt +3 -0
- d365fo_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- d365fo_client-0.1.0.dist-info/top_level.txt +1 -0
d365fo_client/labels.py
ADDED
@@ -0,0 +1,528 @@
|
|
1
|
+
"""Label operations for D365 F&O client."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from typing import Any, Dict, List, Optional, Protocol, Union, runtime_checkable
|
5
|
+
|
6
|
+
from .models import LabelInfo, PublicEntityInfo
|
7
|
+
from .session import SessionManager
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
|
12
|
+
@runtime_checkable
|
13
|
+
class LabelCacheProtocol(Protocol):
|
14
|
+
"""Protocol for label caching implementations."""
|
15
|
+
|
16
|
+
async def get_label(self, label_id: str, language: str) -> Optional[str]:
|
17
|
+
"""Get a single label from cache."""
|
18
|
+
...
|
19
|
+
|
20
|
+
async def set_label(self, label_info: LabelInfo) -> None:
|
21
|
+
"""Set a single label in cache."""
|
22
|
+
...
|
23
|
+
|
24
|
+
async def set_labels_batch(self, labels: List[LabelInfo]) -> None:
|
25
|
+
"""Set multiple labels in cache."""
|
26
|
+
...
|
27
|
+
|
28
|
+
async def get_labels_batch(
|
29
|
+
self, label_ids: List[str], language: str
|
30
|
+
) -> Dict[str, str]:
|
31
|
+
"""Resolve multiple label IDs to their text values."""
|
32
|
+
...
|
33
|
+
|
34
|
+
|
35
|
+
class LabelOperations:
|
36
|
+
"""Handles label operations for F&O client"""
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
session_manager: SessionManager,
|
41
|
+
metadata_url: str,
|
42
|
+
label_cache: Optional[LabelCacheProtocol] = None,
|
43
|
+
):
|
44
|
+
"""Initialize label operations
|
45
|
+
|
46
|
+
Args:
|
47
|
+
session_manager: HTTP session manager
|
48
|
+
metadata_url: Metadata API URL
|
49
|
+
label_cache: Optional label cache implementing LabelCacheProtocol
|
50
|
+
"""
|
51
|
+
self.session_manager = session_manager
|
52
|
+
self.metadata_url = metadata_url
|
53
|
+
self.label_cache = label_cache
|
54
|
+
|
55
|
+
def set_label_cache(self, label_cache: LabelCacheProtocol):
|
56
|
+
"""Set the label cache for label operations
|
57
|
+
|
58
|
+
Args:
|
59
|
+
label_cache: Label cache implementing LabelCacheProtocol
|
60
|
+
"""
|
61
|
+
self.label_cache = label_cache
|
62
|
+
|
63
|
+
async def get_label_text(
|
64
|
+
self, label_id: str, language: str = "en-US"
|
65
|
+
) -> Optional[str]:
|
66
|
+
"""Get actual label text for a specific label ID
|
67
|
+
|
68
|
+
Args:
|
69
|
+
label_id: Label ID (e.g., "@SYS13342")
|
70
|
+
language: Language code (e.g., "en-US")
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
Label text or None if not found
|
74
|
+
"""
|
75
|
+
# Check cache first
|
76
|
+
if self.label_cache:
|
77
|
+
cached_value = await self.label_cache.get_label(label_id, language)
|
78
|
+
if cached_value is not None:
|
79
|
+
return cached_value
|
80
|
+
|
81
|
+
try:
|
82
|
+
session = await self.session_manager.get_session()
|
83
|
+
url = f"{self.metadata_url}/Labels(Id='{label_id}',Language='{language}')"
|
84
|
+
|
85
|
+
async with session.get(url) as response:
|
86
|
+
if response.status == 200:
|
87
|
+
data = await response.json()
|
88
|
+
label_text = data.get("Value", "")
|
89
|
+
|
90
|
+
# Cache the result
|
91
|
+
if self.label_cache:
|
92
|
+
label_info = LabelInfo(
|
93
|
+
id=label_id, language=language, value=label_text
|
94
|
+
)
|
95
|
+
await self.label_cache.set_label(label_info)
|
96
|
+
|
97
|
+
return label_text
|
98
|
+
else:
|
99
|
+
print(f"Error fetching label {label_id}: {response.status}")
|
100
|
+
|
101
|
+
except Exception as e:
|
102
|
+
print(f"Exception fetching label {label_id}: {e}")
|
103
|
+
|
104
|
+
return None
|
105
|
+
|
106
|
+
async def get_labels_batch(
|
107
|
+
self, label_ids: List[str], language: str = "en-US"
|
108
|
+
) -> Dict[str, str]:
|
109
|
+
"""Get multiple labels efficiently using batch operations
|
110
|
+
|
111
|
+
Args:
|
112
|
+
label_ids: List of label IDs
|
113
|
+
language: Language code
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Dictionary mapping label ID to label text
|
117
|
+
"""
|
118
|
+
if not label_ids:
|
119
|
+
return {}
|
120
|
+
|
121
|
+
results = {}
|
122
|
+
uncached_ids = []
|
123
|
+
|
124
|
+
# First, check cache for all labels if available
|
125
|
+
if self.label_cache:
|
126
|
+
for label_id in label_ids:
|
127
|
+
cached_value = await self.label_cache.get_label(label_id, language)
|
128
|
+
if cached_value is not None:
|
129
|
+
results[label_id] = cached_value
|
130
|
+
else:
|
131
|
+
uncached_ids.append(label_id)
|
132
|
+
else:
|
133
|
+
uncached_ids = label_ids
|
134
|
+
|
135
|
+
# Fetch uncached labels from API
|
136
|
+
if uncached_ids:
|
137
|
+
# For now, use individual calls - could be optimized with batch API if available
|
138
|
+
fetched_labels = []
|
139
|
+
for label_id in uncached_ids:
|
140
|
+
try:
|
141
|
+
session = await self.session_manager.get_session()
|
142
|
+
url = f"{self.metadata_url}/Labels(Id='{label_id}',Language='{language}')"
|
143
|
+
|
144
|
+
async with session.get(url) as response:
|
145
|
+
if response.status == 200:
|
146
|
+
data = await response.json()
|
147
|
+
label_text = data.get("Value", "")
|
148
|
+
results[label_id] = label_text
|
149
|
+
|
150
|
+
# Prepare for batch cache storage
|
151
|
+
fetched_labels.append(
|
152
|
+
LabelInfo(
|
153
|
+
id=label_id, language=language, value=label_text
|
154
|
+
)
|
155
|
+
)
|
156
|
+
else:
|
157
|
+
print(f"Error fetching label {label_id}: {response.status}")
|
158
|
+
|
159
|
+
except Exception as e:
|
160
|
+
print(f"Exception fetching label {label_id}: {e}")
|
161
|
+
|
162
|
+
# Batch cache all fetched labels
|
163
|
+
if fetched_labels and self.label_cache:
|
164
|
+
await self.label_cache.set_labels_batch(fetched_labels)
|
165
|
+
|
166
|
+
return results
|
167
|
+
|
168
|
+
async def resolve_public_entity_labels(
|
169
|
+
self, entity_info: PublicEntityInfo, language: str
|
170
|
+
):
|
171
|
+
"""Resolve all label IDs in a public entity to actual text
|
172
|
+
|
173
|
+
Args:
|
174
|
+
entity_info: Public entity information object to update
|
175
|
+
language: Language code
|
176
|
+
"""
|
177
|
+
# Collect all label IDs
|
178
|
+
label_ids = []
|
179
|
+
if entity_info.label_id:
|
180
|
+
label_ids.append(entity_info.label_id)
|
181
|
+
|
182
|
+
for prop in entity_info.properties:
|
183
|
+
if prop.label_id:
|
184
|
+
label_ids.append(prop.label_id)
|
185
|
+
|
186
|
+
# Batch fetch all labels
|
187
|
+
if label_ids:
|
188
|
+
labels_map = await self.get_labels_batch(label_ids, language)
|
189
|
+
|
190
|
+
# Apply resolved labels
|
191
|
+
if entity_info.label_id:
|
192
|
+
entity_info.label_text = labels_map.get(entity_info.label_id)
|
193
|
+
|
194
|
+
for prop in entity_info.properties:
|
195
|
+
if prop.label_id:
|
196
|
+
prop.label_text = labels_map.get(prop.label_id)
|
197
|
+
|
198
|
+
|
199
|
+
# Generic Label Resolution Utility Functions
|
200
|
+
|
201
|
+
|
202
|
+
async def resolve_labels_generic(
|
203
|
+
obj_or_list: Union[Any, List[Any]],
|
204
|
+
label_operations: LabelOperations,
|
205
|
+
language: str = "en-US",
|
206
|
+
) -> Union[Any, List[Any]]:
|
207
|
+
"""
|
208
|
+
Generic utility function to resolve label IDs to label text for any object(s)
|
209
|
+
containing label_id and label_text properties.
|
210
|
+
|
211
|
+
Args:
|
212
|
+
obj_or_list: Single object or list of objects with label_id/label_text properties
|
213
|
+
label_operations: LabelOperations instance for label resolution
|
214
|
+
language: Language code for label resolution (default: "en-US")
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
The same object(s) with label_text populated from label_id where applicable
|
218
|
+
|
219
|
+
Examples:
|
220
|
+
# Single object
|
221
|
+
entity = await resolve_labels_generic(entity, label_ops)
|
222
|
+
|
223
|
+
# List of objects
|
224
|
+
entities = await resolve_labels_generic(entities, label_ops)
|
225
|
+
|
226
|
+
# Works with any object type that has label_id/label_text attributes
|
227
|
+
properties = await resolve_labels_generic(properties, label_ops, "fr-FR")
|
228
|
+
"""
|
229
|
+
logger.debug(f"Starting generic label resolution for language: {language}")
|
230
|
+
|
231
|
+
if obj_or_list is None:
|
232
|
+
logger.debug("Input is None, returning unchanged")
|
233
|
+
return obj_or_list
|
234
|
+
|
235
|
+
# Handle list of objects
|
236
|
+
if isinstance(obj_or_list, list):
|
237
|
+
if not obj_or_list:
|
238
|
+
logger.debug("Input is empty list, returning unchanged")
|
239
|
+
return obj_or_list
|
240
|
+
|
241
|
+
logger.debug(f"Processing list with {len(obj_or_list)} objects")
|
242
|
+
|
243
|
+
# Collect all label IDs from all objects in the list
|
244
|
+
label_ids = set()
|
245
|
+
for i, obj in enumerate(obj_or_list):
|
246
|
+
obj_type = type(obj).__name__ if obj else "None"
|
247
|
+
logger.debug(f"Collecting labels from list item {i} (type: {obj_type})")
|
248
|
+
_collect_label_ids_from_object(obj, label_ids)
|
249
|
+
|
250
|
+
logger.debug(
|
251
|
+
f"Collected {len(label_ids)} unique label IDs from list: {list(label_ids)}"
|
252
|
+
)
|
253
|
+
|
254
|
+
# Resolve all labels in batch
|
255
|
+
label_texts = await _resolve_labels_batch(
|
256
|
+
list(label_ids), label_operations, language
|
257
|
+
)
|
258
|
+
|
259
|
+
logger.debug(
|
260
|
+
f"Successfully resolved {len(label_texts)} labels out of {len(label_ids)} requested"
|
261
|
+
)
|
262
|
+
|
263
|
+
# Apply resolved labels to all objects
|
264
|
+
for i, obj in enumerate(obj_or_list):
|
265
|
+
logger.debug(f"Applying labels to list item {i}")
|
266
|
+
_apply_labels_to_object(obj, label_texts)
|
267
|
+
|
268
|
+
logger.debug(
|
269
|
+
f"Completed label resolution for list with {len(obj_or_list)} objects"
|
270
|
+
)
|
271
|
+
return obj_or_list
|
272
|
+
|
273
|
+
# Handle single object
|
274
|
+
else:
|
275
|
+
obj_type = type(obj_or_list).__name__
|
276
|
+
logger.debug(f"Processing single object of type: {obj_type}")
|
277
|
+
|
278
|
+
# Collect label IDs from the object
|
279
|
+
label_ids = set()
|
280
|
+
_collect_label_ids_from_object(obj_or_list, label_ids)
|
281
|
+
|
282
|
+
logger.debug(
|
283
|
+
f"Collected {len(label_ids)} label IDs from object: {list(label_ids)}"
|
284
|
+
)
|
285
|
+
|
286
|
+
# Resolve labels in batch
|
287
|
+
label_texts = await _resolve_labels_batch(
|
288
|
+
list(label_ids), label_operations, language
|
289
|
+
)
|
290
|
+
|
291
|
+
logger.debug(
|
292
|
+
f"Successfully resolved {len(label_texts)} labels out of {len(label_ids)} requested"
|
293
|
+
)
|
294
|
+
|
295
|
+
# Apply resolved labels
|
296
|
+
_apply_labels_to_object(obj_or_list, label_texts)
|
297
|
+
|
298
|
+
logger.debug(f"Completed label resolution for single {obj_type} object")
|
299
|
+
return obj_or_list
|
300
|
+
|
301
|
+
|
302
|
+
def _collect_label_ids_from_object(obj: Any, label_ids: set) -> None:
|
303
|
+
"""
|
304
|
+
Recursively collect all label_id values from an object and its nested objects/lists.
|
305
|
+
|
306
|
+
Args:
|
307
|
+
obj: Object to collect label IDs from
|
308
|
+
label_ids: Set to store collected label IDs
|
309
|
+
"""
|
310
|
+
if obj is None:
|
311
|
+
logger.debug("Skipping None object in label collection")
|
312
|
+
return
|
313
|
+
|
314
|
+
obj_type = type(obj).__name__
|
315
|
+
initial_count = len(label_ids)
|
316
|
+
|
317
|
+
# Check if object has label_id attribute
|
318
|
+
if hasattr(obj, "label_id") and obj.label_id:
|
319
|
+
logger.debug(f"Found label_id '{obj.label_id}' in {obj_type} object")
|
320
|
+
label_ids.add(obj.label_id)
|
321
|
+
else:
|
322
|
+
logger.debug(f"{obj_type} object has no label_id or label_id is empty")
|
323
|
+
|
324
|
+
# Recursively check common nested attributes that might contain labeled objects
|
325
|
+
nested_attrs = [
|
326
|
+
"properties",
|
327
|
+
"members",
|
328
|
+
"navigation_properties",
|
329
|
+
"property_groups",
|
330
|
+
"actions",
|
331
|
+
"parameters",
|
332
|
+
"constraints",
|
333
|
+
"enhanced_properties",
|
334
|
+
]
|
335
|
+
|
336
|
+
for attr_name in nested_attrs:
|
337
|
+
if hasattr(obj, attr_name):
|
338
|
+
nested_obj = getattr(obj, attr_name)
|
339
|
+
if isinstance(nested_obj, list):
|
340
|
+
if nested_obj:
|
341
|
+
logger.debug(
|
342
|
+
f"Recursively collecting from {len(nested_obj)} items in {attr_name}"
|
343
|
+
)
|
344
|
+
for i, item in enumerate(nested_obj):
|
345
|
+
logger.debug(f" Processing {attr_name}[{i}]")
|
346
|
+
_collect_label_ids_from_object(item, label_ids)
|
347
|
+
else:
|
348
|
+
logger.debug(f"{attr_name} is empty list")
|
349
|
+
elif nested_obj is not None:
|
350
|
+
logger.debug(f"Recursively collecting from {attr_name} (single object)")
|
351
|
+
_collect_label_ids_from_object(nested_obj, label_ids)
|
352
|
+
else:
|
353
|
+
logger.debug(f"{attr_name} is None")
|
354
|
+
|
355
|
+
collected_count = len(label_ids) - initial_count
|
356
|
+
if collected_count > 0:
|
357
|
+
logger.debug(
|
358
|
+
f"Collected {collected_count} new label IDs from {obj_type} object"
|
359
|
+
)
|
360
|
+
else:
|
361
|
+
logger.debug(f"No new label IDs found in {obj_type} object")
|
362
|
+
|
363
|
+
|
364
|
+
async def _resolve_labels_batch(
|
365
|
+
label_ids: List[str], label_operations: LabelOperations, language: str
|
366
|
+
) -> Dict[str, str]:
|
367
|
+
"""
|
368
|
+
Resolve multiple label IDs to their text values in batch using LabelOperations.
|
369
|
+
|
370
|
+
Args:
|
371
|
+
label_ids: List of label IDs to resolve
|
372
|
+
label_operations: LabelOperations instance
|
373
|
+
language: Language code
|
374
|
+
|
375
|
+
Returns:
|
376
|
+
Dictionary mapping label_id to label_text
|
377
|
+
"""
|
378
|
+
if not label_ids:
|
379
|
+
logger.debug("No label IDs to resolve")
|
380
|
+
return {}
|
381
|
+
|
382
|
+
logger.debug(
|
383
|
+
f"Starting batch resolution of {len(label_ids)} label IDs in language '{language}'"
|
384
|
+
)
|
385
|
+
logger.debug(f"Label IDs to resolve: {label_ids}")
|
386
|
+
|
387
|
+
# Use LabelOperations batch method for efficient resolution
|
388
|
+
try:
|
389
|
+
label_texts = await label_operations.get_labels_batch(label_ids, language)
|
390
|
+
|
391
|
+
successful_resolutions = len(label_texts)
|
392
|
+
failed_resolutions = len(label_ids) - successful_resolutions
|
393
|
+
|
394
|
+
logger.info(
|
395
|
+
f"Batch label resolution completed: {successful_resolutions} successful, {failed_resolutions} failed out of {len(label_ids)} total"
|
396
|
+
)
|
397
|
+
|
398
|
+
if successful_resolutions > 0:
|
399
|
+
logger.debug(f"Successfully resolved labels: {list(label_texts.keys())}")
|
400
|
+
|
401
|
+
# Log individual results at debug level
|
402
|
+
for label_id in label_ids:
|
403
|
+
if label_id in label_texts:
|
404
|
+
logger.debug(f"✅ Resolved '{label_id}' -> '{label_texts[label_id]}'")
|
405
|
+
else:
|
406
|
+
logger.debug(f"❌ No text found for label ID '{label_id}'")
|
407
|
+
|
408
|
+
return label_texts
|
409
|
+
|
410
|
+
except Exception as e:
|
411
|
+
logger.warning(f"❌ Error in batch label resolution: {e}")
|
412
|
+
return {}
|
413
|
+
|
414
|
+
|
415
|
+
def _apply_labels_to_object(obj: Any, label_texts: Dict[str, str]) -> None:
|
416
|
+
"""
|
417
|
+
Recursively apply resolved labels to an object and its nested objects/lists.
|
418
|
+
|
419
|
+
Args:
|
420
|
+
obj: Object to apply labels to
|
421
|
+
label_texts: Dictionary mapping label_id to label_text
|
422
|
+
"""
|
423
|
+
if obj is None:
|
424
|
+
logger.debug("Skipping None object in label application")
|
425
|
+
return
|
426
|
+
|
427
|
+
obj_type = type(obj).__name__
|
428
|
+
labels_applied = 0
|
429
|
+
|
430
|
+
# Apply label to current object if it has label_id and label_text attributes
|
431
|
+
if (
|
432
|
+
hasattr(obj, "label_id")
|
433
|
+
and hasattr(obj, "label_text")
|
434
|
+
and obj.label_id
|
435
|
+
and obj.label_id in label_texts
|
436
|
+
):
|
437
|
+
old_text = obj.label_text
|
438
|
+
obj.label_text = label_texts[obj.label_id]
|
439
|
+
labels_applied += 1
|
440
|
+
logger.debug(
|
441
|
+
f"Applied label to {obj_type}: '{obj.label_id}' -> '{obj.label_text}' (was: {old_text})"
|
442
|
+
)
|
443
|
+
elif hasattr(obj, "label_id") and obj.label_id:
|
444
|
+
if not hasattr(obj, "label_text"):
|
445
|
+
logger.debug(
|
446
|
+
f"{obj_type} has label_id '{obj.label_id}' but no label_text attribute"
|
447
|
+
)
|
448
|
+
elif obj.label_id not in label_texts:
|
449
|
+
logger.debug(
|
450
|
+
f"{obj_type} has label_id '{obj.label_id}' but no resolved text available"
|
451
|
+
)
|
452
|
+
|
453
|
+
# Recursively apply to nested attributes
|
454
|
+
nested_attrs = [
|
455
|
+
"properties",
|
456
|
+
"members",
|
457
|
+
"navigation_properties",
|
458
|
+
"property_groups",
|
459
|
+
"actions",
|
460
|
+
"parameters",
|
461
|
+
"constraints",
|
462
|
+
"enhanced_properties",
|
463
|
+
]
|
464
|
+
|
465
|
+
for attr_name in nested_attrs:
|
466
|
+
if hasattr(obj, attr_name):
|
467
|
+
nested_obj = getattr(obj, attr_name)
|
468
|
+
if isinstance(nested_obj, list):
|
469
|
+
if nested_obj:
|
470
|
+
logger.debug(
|
471
|
+
f"Applying labels to {len(nested_obj)} items in {attr_name}"
|
472
|
+
)
|
473
|
+
for i, item in enumerate(nested_obj):
|
474
|
+
logger.debug(f" Applying to {attr_name}[{i}]")
|
475
|
+
_apply_labels_to_object(item, label_texts)
|
476
|
+
elif nested_obj is not None:
|
477
|
+
logger.debug(f"Applying labels to {attr_name} (single object)")
|
478
|
+
_apply_labels_to_object(nested_obj, label_texts)
|
479
|
+
|
480
|
+
if labels_applied > 0:
|
481
|
+
logger.debug(f"Applied {labels_applied} labels to {obj_type} object")
|
482
|
+
else:
|
483
|
+
logger.debug(f"No labels applied to {obj_type} object")
|
484
|
+
|
485
|
+
|
486
|
+
# Utility function for resolving labels with any cache implementation
|
487
|
+
async def resolve_labels_generic_with_cache(
|
488
|
+
obj_or_list: Union[Any, List[Any]],
|
489
|
+
cache: LabelCacheProtocol,
|
490
|
+
language: str = "en-US",
|
491
|
+
) -> Union[Any, List[Any]]:
|
492
|
+
"""
|
493
|
+
Resolve labels using any cache implementation that follows LabelCacheProtocol.
|
494
|
+
|
495
|
+
Args:
|
496
|
+
obj_or_list: Single object or list of objects with label_id/label_text properties
|
497
|
+
cache: Any cache implementing LabelCacheProtocol (MetadataCache, MetadataCacheV2, etc.)
|
498
|
+
language: Language code for label resolution (default: "en-US")
|
499
|
+
|
500
|
+
Returns:
|
501
|
+
The same object(s) with label_text populated from label_id where applicable
|
502
|
+
|
503
|
+
Examples:
|
504
|
+
# Works with any cache implementation
|
505
|
+
entities = await resolve_labels_generic_with_cache(entities, metadata_cache_v2)
|
506
|
+
entities = await resolve_labels_generic_with_cache(entities, old_metadata_cache)
|
507
|
+
"""
|
508
|
+
|
509
|
+
# Create a minimal LabelOperations-like resolver using the cache
|
510
|
+
class CacheLabelResolver:
|
511
|
+
def __init__(self, cache: LabelCacheProtocol):
|
512
|
+
self.cache = cache
|
513
|
+
|
514
|
+
async def get_labels_batch(
|
515
|
+
self, label_ids: List[str], language: str
|
516
|
+
) -> Dict[str, str]:
|
517
|
+
"""Get labels using the cache directly"""
|
518
|
+
label_texts = {}
|
519
|
+
for label_id in label_ids:
|
520
|
+
if label_id:
|
521
|
+
label_text = await self.cache.get_label(label_id, language)
|
522
|
+
if label_text:
|
523
|
+
label_texts[label_id] = label_text
|
524
|
+
return label_texts
|
525
|
+
|
526
|
+
# Use the generic function with the cache resolver
|
527
|
+
cache_resolver = CacheLabelResolver(cache)
|
528
|
+
return await resolve_labels_generic(obj_or_list, cache_resolver, language)
|