d365fo-client 0.1.0__py3-none-any.whl → 0.2.2__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.
@@ -1,10 +1,9 @@
1
1
  """Version-aware metadata cache implementation."""
2
2
 
3
- import json
4
3
  import logging
5
4
  from datetime import datetime, timezone
6
5
  from pathlib import Path
7
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
6
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
8
7
 
9
8
  import aiosqlite
10
9
 
@@ -23,9 +22,7 @@ from ..models import (
23
22
  EnumerationMemberInfo,
24
23
  EnvironmentVersionInfo,
25
24
  FixedConstraintInfo,
26
- GlobalVersionInfo,
27
25
  LabelInfo,
28
- ModuleVersionInfo,
29
26
  NavigationPropertyInfo,
30
27
  ODataBindingKind,
31
28
  PropertyGroupInfo,
@@ -34,11 +31,10 @@ from ..models import (
34
31
  PublicEntityPropertyInfo,
35
32
  ReferentialConstraintInfo,
36
33
  RelatedFixedConstraintInfo,
37
- RelationConstraintInfo,
38
- VersionDetectionResult,
39
34
  )
40
35
  from .database_v2 import MetadataDatabaseV2
41
36
  from .global_version_manager import GlobalVersionManager
37
+ from .label_utils import apply_label_fallback, process_label_fallback
42
38
  from .version_detector import ModuleVersionDetector
43
39
 
44
40
  logger = logging.getLogger(__name__)
@@ -223,8 +219,11 @@ class MetadataCacheV2:
223
219
  (global_version_id,),
224
220
  )
225
221
 
226
- # Insert new entities
222
+ # Insert new entities with label processing
227
223
  for entity in entities:
224
+ # Process label fallback for this entity
225
+ processed_label_text = process_label_fallback(entity.label_id, entity.label_text)
226
+
228
227
  await db.execute(
229
228
  """INSERT INTO data_entities
230
229
  (global_version_id, name, public_entity_name, public_collection_name,
@@ -237,8 +236,8 @@ class MetadataCacheV2:
237
236
  entity.public_entity_name,
238
237
  entity.public_collection_name,
239
238
  entity.label_id,
240
- entity.label_text,
241
- entity.entity_category,
239
+ processed_label_text, # Use processed label text
240
+ entity.entity_category if entity.entity_category else None,
242
241
  entity.data_service_enabled,
243
242
  entity.data_management_enabled,
244
243
  entity.is_read_only,
@@ -309,13 +308,16 @@ class MetadataCacheV2:
309
308
 
310
309
  entities = []
311
310
  for row in await cursor.fetchall():
311
+ # Apply label fallback during retrieval
312
+ processed_label_text = apply_label_fallback(row[3], row[4])
313
+
312
314
  entities.append(
313
315
  DataEntityInfo(
314
316
  name=row[0],
315
317
  public_entity_name=row[1],
316
318
  public_collection_name=row[2],
317
319
  label_id=row[3],
318
- label_text=row[4],
320
+ label_text=processed_label_text,
319
321
  entity_category=row[5],
320
322
  data_service_enabled=row[6],
321
323
  data_management_enabled=row[7],
@@ -406,7 +408,9 @@ class MetadataCacheV2:
406
408
  (existing_entity_id, global_version_id),
407
409
  )
408
410
 
409
- # Insert new entity
411
+ # Insert new entity with label processing
412
+ processed_entity_label_text = process_label_fallback(entity_schema.label_id, entity_schema.label_text)
413
+
410
414
  cursor = await db.execute(
411
415
  """INSERT INTO public_entities
412
416
  (global_version_id, name, entity_set_name, label_id, label_text,
@@ -417,7 +421,7 @@ class MetadataCacheV2:
417
421
  entity_schema.name,
418
422
  entity_schema.entity_set_name,
419
423
  entity_schema.label_id,
420
- entity_schema.label_text,
424
+ processed_entity_label_text, # Use processed label text
421
425
  entity_schema.is_read_only,
422
426
  entity_schema.configuration_enabled,
423
427
  ),
@@ -425,10 +429,13 @@ class MetadataCacheV2:
425
429
 
426
430
  entity_id = cursor.lastrowid
427
431
 
428
- # Store properties
432
+ # Store properties with label processing
429
433
  prop_order = 0
430
434
  for prop in entity_schema.properties:
431
435
  prop_order += 1
436
+ # Process label fallback for this property
437
+ processed_prop_label_text = process_label_fallback(prop.label_id, prop.label_text)
438
+
432
439
  await db.execute(
433
440
  """INSERT INTO entity_properties
434
441
  (entity_id, global_version_id, name, type_name, data_type,
@@ -446,7 +453,7 @@ class MetadataCacheV2:
446
453
  prop.data_type,
447
454
  prop.data_type,
448
455
  prop.label_id,
449
- prop.label_text,
456
+ processed_prop_label_text, # Use processed label text
450
457
  prop.is_key,
451
458
  prop.is_mandatory,
452
459
  prop.configuration_enabled,
@@ -474,7 +481,7 @@ class MetadataCacheV2:
474
481
  nav_prop.name,
475
482
  nav_prop.related_entity,
476
483
  nav_prop.related_relation_name,
477
- nav_prop.cardinality.value, # Convert enum to string value
484
+ nav_prop.cardinality, # StrEnum automatically converts to string
478
485
  ),
479
486
  )
480
487
 
@@ -512,7 +519,7 @@ class MetadataCacheV2:
512
519
  entity_id,
513
520
  global_version_id,
514
521
  action.name,
515
- action.binding_kind.value, # Convert enum to string value
522
+ action.binding_kind, # StrEnum automatically converts to string
516
523
  entity_schema.name,
517
524
  entity_schema.entity_set_name,
518
525
  action.return_type.type_name if action.return_type else None,
@@ -624,6 +631,9 @@ class MetadataCacheV2:
624
631
 
625
632
  properties = []
626
633
  for prop_row in await cursor.fetchall():
634
+ # Apply label fallback for property labels
635
+ processed_prop_label_text = apply_label_fallback(prop_row[4], prop_row[5])
636
+
627
637
  properties.append(
628
638
  PublicEntityPropertyInfo(
629
639
  name=prop_row[0],
@@ -631,7 +641,7 @@ class MetadataCacheV2:
631
641
  data_type=prop_row[2],
632
642
  odata_xpp_type=prop_row[3],
633
643
  label_id=prop_row[4],
634
- label_text=prop_row[5],
644
+ label_text=processed_prop_label_text,
635
645
  is_key=prop_row[6],
636
646
  is_mandatory=prop_row[7],
637
647
  configuration_enabled=prop_row[8],
@@ -794,11 +804,14 @@ class MetadataCacheV2:
794
804
  )
795
805
  )
796
806
 
807
+ # Apply label fallback for entity labels
808
+ processed_entity_label_text = apply_label_fallback(entity_row[3], entity_row[4])
809
+
797
810
  return PublicEntityInfo(
798
811
  name=entity_row[1],
799
812
  entity_set_name=entity_row[2],
800
813
  label_id=entity_row[3],
801
- label_text=entity_row[4],
814
+ label_text=processed_entity_label_text,
802
815
  is_read_only=entity_row[5],
803
816
  configuration_enabled=entity_row[6],
804
817
  properties=properties,
@@ -824,6 +837,9 @@ class MetadataCacheV2:
824
837
  )
825
838
 
826
839
  for enum_info in enumerations:
840
+ # Process label fallback for this enumeration
841
+ processed_enum_label_text = process_label_fallback(enum_info.label_id, enum_info.label_text)
842
+
827
843
  # Insert enumeration
828
844
  cursor = await db.execute(
829
845
  """INSERT INTO enumerations
@@ -833,16 +849,19 @@ class MetadataCacheV2:
833
849
  global_version_id,
834
850
  enum_info.name,
835
851
  enum_info.label_id,
836
- enum_info.label_text,
852
+ processed_enum_label_text, # Use processed label text
837
853
  ),
838
854
  )
839
855
 
840
856
  enum_id = cursor.lastrowid
841
857
 
842
- # Insert members
858
+ # Insert members with label processing
843
859
  member_order = 0
844
860
  for member in enum_info.members:
845
861
  member_order += 1
862
+ # Process label fallback for this member
863
+ processed_member_label_text = process_label_fallback(member.label_id, member.label_text)
864
+
846
865
  await db.execute(
847
866
  """INSERT INTO enumeration_members
848
867
  (enumeration_id, global_version_id, name, value,
@@ -854,7 +873,7 @@ class MetadataCacheV2:
854
873
  member.name,
855
874
  member.value,
856
875
  member.label_id,
857
- member.label_text,
876
+ processed_member_label_text, # Use processed label text
858
877
  member.configuration_enabled,
859
878
  member_order,
860
879
  ),
@@ -908,21 +927,27 @@ class MetadataCacheV2:
908
927
 
909
928
  members = []
910
929
  for member_row in await cursor.fetchall():
930
+ # Apply label fallback for member labels
931
+ processed_member_label_text = apply_label_fallback(member_row[2], member_row[3])
932
+
911
933
  members.append(
912
934
  EnumerationMemberInfo(
913
935
  name=member_row[0],
914
936
  value=member_row[1],
915
937
  label_id=member_row[2],
916
- label_text=member_row[3],
938
+ label_text=processed_member_label_text,
917
939
  configuration_enabled=member_row[4],
918
940
  member_order=member_row[5],
919
941
  )
920
942
  )
921
943
 
944
+ # Apply label fallback for enumeration labels
945
+ processed_enum_label_text = apply_label_fallback(enum_row[2], enum_row[3])
946
+
922
947
  return EnumerationInfo(
923
948
  name=enum_row[1],
924
949
  label_id=enum_row[2],
925
- label_text=enum_row[3],
950
+ label_text=processed_enum_label_text,
926
951
  members=members,
927
952
  )
928
953
 
@@ -984,6 +1009,203 @@ class MetadataCacheV2:
984
1009
 
985
1010
  return None
986
1011
 
1012
+ # Action Operations
1013
+
1014
+ async def search_actions(
1015
+ self,
1016
+ pattern: Optional[str] = None,
1017
+ entity_name: Optional[str] = None,
1018
+ binding_kind: Optional[str] = None,
1019
+ global_version_id: Optional[int] = None,
1020
+ ) -> List[ActionInfo]:
1021
+ """Search for actions with filtering
1022
+
1023
+ Args:
1024
+ pattern: Search pattern for action name (SQL LIKE)
1025
+ entity_name: Filter by entity name
1026
+ binding_kind: Filter by binding kind
1027
+ global_version_id: Global version ID (uses current if None)
1028
+
1029
+ Returns:
1030
+ List of matching actions
1031
+ """
1032
+ if global_version_id is None:
1033
+ global_version_id = await self._get_current_global_version_id()
1034
+ if global_version_id is None:
1035
+ return []
1036
+
1037
+ # Build query conditions
1038
+ conditions = ["ea.global_version_id = ?"]
1039
+ params = [global_version_id]
1040
+
1041
+ if pattern is not None:
1042
+ conditions.append("ea.name LIKE ?")
1043
+ params.append(pattern)
1044
+
1045
+ if entity_name is not None:
1046
+ conditions.append("ea.entity_name = ?")
1047
+ params.append(entity_name)
1048
+
1049
+ if binding_kind is not None:
1050
+ conditions.append("ea.binding_kind = ?")
1051
+ params.append(binding_kind)
1052
+
1053
+ where_clause = " AND ".join(conditions)
1054
+
1055
+ async with aiosqlite.connect(self.db_path) as db:
1056
+ cursor = await db.execute(
1057
+ f"""SELECT ea.name, ea.binding_kind, ea.entity_name,
1058
+ ea.entity_set_name, ea.return_type_name,
1059
+ ea.return_is_collection, ea.return_odata_xpp_type,
1060
+ ea.field_lookup
1061
+ FROM entity_actions ea
1062
+ WHERE {where_clause}
1063
+ ORDER BY ea.name""",
1064
+ params,
1065
+ )
1066
+
1067
+ rows = await cursor.fetchall()
1068
+ actions = []
1069
+
1070
+ for row in rows:
1071
+ # Get parameters for this action
1072
+ param_cursor = await db.execute(
1073
+ """SELECT name, type_name, is_collection, odata_xpp_type
1074
+ FROM action_parameters
1075
+ WHERE action_id = (
1076
+ SELECT id FROM entity_actions
1077
+ WHERE name = ? AND entity_name = ? AND global_version_id = ?
1078
+ )
1079
+ ORDER BY parameter_order""",
1080
+ (row[0], row[2], global_version_id),
1081
+ )
1082
+ param_rows = await param_cursor.fetchall()
1083
+
1084
+ parameters = [
1085
+ ActionParameterInfo(
1086
+ name=param_row[0],
1087
+ type=ActionParameterTypeInfo(
1088
+ type_name=param_row[1],
1089
+ is_collection=bool(param_row[2]),
1090
+ odata_xpp_type=param_row[3],
1091
+ ),
1092
+ )
1093
+ for param_row in param_rows
1094
+ ]
1095
+
1096
+ # Create return type if present
1097
+ return_type = None
1098
+ if row[4]: # return_type_name
1099
+ return_type = ActionReturnTypeInfo(
1100
+ type_name=row[4],
1101
+ is_collection=bool(row[5]),
1102
+ odata_xpp_type=row[6],
1103
+ )
1104
+
1105
+ action = ActionInfo(
1106
+ name=row[0],
1107
+ binding_kind=ODataBindingKind(row[1]),
1108
+ entity_name=row[2],
1109
+ entity_set_name=row[3],
1110
+ parameters=parameters,
1111
+ return_type=return_type,
1112
+ field_lookup=row[7],
1113
+ )
1114
+
1115
+ actions.append(action)
1116
+
1117
+ return actions
1118
+
1119
+ async def get_action_info(
1120
+ self,
1121
+ action_name: str,
1122
+ entity_name: Optional[str] = None,
1123
+ global_version_id: Optional[int] = None,
1124
+ ) -> Optional[ActionInfo]:
1125
+ """Get specific action information
1126
+
1127
+ Args:
1128
+ action_name: Name of the action
1129
+ entity_name: Entity name for bound actions
1130
+ global_version_id: Global version ID (uses current if None)
1131
+
1132
+ Returns:
1133
+ Action information if found, None otherwise
1134
+ """
1135
+ if global_version_id is None:
1136
+ global_version_id = await self._get_current_global_version_id()
1137
+ if global_version_id is None:
1138
+ return None
1139
+
1140
+ # Build query conditions
1141
+ conditions = ["ea.global_version_id = ?", "ea.name = ?"]
1142
+ params = [global_version_id, action_name]
1143
+
1144
+ if entity_name is not None:
1145
+ conditions.append("ea.entity_name = ?")
1146
+ params.append(entity_name)
1147
+
1148
+ where_clause = " AND ".join(conditions)
1149
+
1150
+ async with aiosqlite.connect(self.db_path) as db:
1151
+ cursor = await db.execute(
1152
+ f"""SELECT ea.id, ea.name, ea.binding_kind, ea.entity_name,
1153
+ ea.entity_set_name, ea.return_type_name,
1154
+ ea.return_is_collection, ea.return_odata_xpp_type,
1155
+ ea.field_lookup
1156
+ FROM entity_actions ea
1157
+ WHERE {where_clause}
1158
+ LIMIT 1""",
1159
+ params,
1160
+ )
1161
+
1162
+ row = await cursor.fetchone()
1163
+ if not row:
1164
+ return None
1165
+
1166
+ action_id = row[0]
1167
+
1168
+ # Get parameters for this action
1169
+ param_cursor = await db.execute(
1170
+ """SELECT name, type_name, is_collection, odata_xpp_type
1171
+ FROM action_parameters
1172
+ WHERE action_id = ?
1173
+ ORDER BY parameter_order""",
1174
+ (action_id,),
1175
+ )
1176
+ param_rows = await param_cursor.fetchall()
1177
+
1178
+ parameters = [
1179
+ ActionParameterInfo(
1180
+ name=param_row[0],
1181
+ type=ActionParameterTypeInfo(
1182
+ type_name=param_row[1],
1183
+ is_collection=bool(param_row[2]),
1184
+ odata_xpp_type=param_row[3],
1185
+ ),
1186
+ )
1187
+ for param_row in param_rows
1188
+ ]
1189
+
1190
+ # Create return type if present
1191
+ return_type = None
1192
+ if row[5]: # return_type_name
1193
+ return_type = ActionReturnTypeInfo(
1194
+ type_name=row[5],
1195
+ is_collection=bool(row[6]),
1196
+ odata_xpp_type=row[7],
1197
+ )
1198
+
1199
+ return ActionInfo(
1200
+ name=row[1],
1201
+ binding_kind=ODataBindingKind(row[2]),
1202
+ entity_name=row[3],
1203
+ entity_set_name=row[4],
1204
+ parameters=parameters,
1205
+ return_type=return_type,
1206
+ field_lookup=row[8],
1207
+ )
1208
+
987
1209
  # Label Operations
988
1210
 
989
1211
  async def get_label(
@@ -1000,7 +1222,7 @@ class MetadataCacheV2:
1000
1222
  global_version_id: Global version ID (uses current if None, includes temporary entries)
1001
1223
 
1002
1224
  Returns:
1003
- Label text or None if not found or expired
1225
+ Label text or None if not found
1004
1226
  """
1005
1227
  if global_version_id is None:
1006
1228
  global_version_id = await self._get_current_global_version_id()
@@ -1009,19 +1231,17 @@ class MetadataCacheV2:
1009
1231
  if global_version_id is not None:
1010
1232
  # Search for specific version
1011
1233
  cursor = await db.execute(
1012
- """SELECT label_text, expires_at
1234
+ """SELECT label_text
1013
1235
  FROM labels_cache
1014
- WHERE global_version_id = ? AND label_id = ? AND language = ?
1015
- AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)""",
1236
+ WHERE global_version_id = ? AND label_id = ? AND language = ?""",
1016
1237
  (global_version_id, label_id, language),
1017
1238
  )
1018
1239
  else:
1019
1240
  # Search across all versions (including temporary entries)
1020
1241
  cursor = await db.execute(
1021
- """SELECT label_text, expires_at
1242
+ """SELECT label_text
1022
1243
  FROM labels_cache
1023
1244
  WHERE label_id = ? AND language = ?
1024
- AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
1025
1245
  ORDER BY global_version_id DESC
1026
1246
  LIMIT 1""", # Get the highest version (prefer actual versions over temporary)
1027
1247
  (label_id, language),
@@ -1059,7 +1279,6 @@ class MetadataCacheV2:
1059
1279
  label_text: str,
1060
1280
  language: str = "en-US",
1061
1281
  global_version_id: Optional[int] = None,
1062
- ttl_hours: int = 24,
1063
1282
  ):
1064
1283
  """Set label in cache
1065
1284
 
@@ -1068,7 +1287,6 @@ class MetadataCacheV2:
1068
1287
  label_text: Label text value
1069
1288
  language: Language code
1070
1289
  global_version_id: Global version ID (uses current if None)
1071
- ttl_hours: Time to live in hours
1072
1290
  """
1073
1291
  if global_version_id is None:
1074
1292
  global_version_id = await self._get_current_global_version_id()
@@ -1079,22 +1297,16 @@ class MetadataCacheV2:
1079
1297
  )
1080
1298
  global_version_id = -1 # Use -1 for temporary entries
1081
1299
 
1082
- # Calculate expiration time
1083
- from datetime import datetime, timedelta, timezone
1084
-
1085
- expires_at = datetime.now(timezone.utc) + timedelta(hours=ttl_hours)
1086
-
1087
1300
  async with aiosqlite.connect(self.db_path) as db:
1088
1301
  await db.execute(
1089
1302
  """INSERT OR REPLACE INTO labels_cache
1090
- (global_version_id, label_id, language, label_text, expires_at, hit_count, last_accessed)
1091
- VALUES (?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP)""",
1303
+ (global_version_id, label_id, language, label_text, hit_count, last_accessed)
1304
+ VALUES (?, ?, ?, ?, 0, CURRENT_TIMESTAMP)""",
1092
1305
  (
1093
1306
  global_version_id,
1094
1307
  label_id,
1095
1308
  language,
1096
1309
  label_text,
1097
- expires_at.isoformat(),
1098
1310
  ),
1099
1311
  )
1100
1312
  await db.commit()
@@ -1105,14 +1317,12 @@ class MetadataCacheV2:
1105
1317
  self,
1106
1318
  labels: List[LabelInfo],
1107
1319
  global_version_id: Optional[int] = None,
1108
- ttl_hours: int = 24,
1109
1320
  ):
1110
1321
  """Set multiple labels in cache efficiently
1111
1322
 
1112
1323
  Args:
1113
1324
  labels: List of LabelInfo objects
1114
1325
  global_version_id: Global version ID (uses current if None)
1115
- ttl_hours: Time to live in hours
1116
1326
  """
1117
1327
  if not labels:
1118
1328
  return
@@ -1126,11 +1336,6 @@ class MetadataCacheV2:
1126
1336
  )
1127
1337
  global_version_id = -1 # Use -1 for temporary entries
1128
1338
 
1129
- # Calculate expiration time
1130
- from datetime import datetime, timedelta, timezone
1131
-
1132
- expires_at = datetime.now(timezone.utc) + timedelta(hours=ttl_hours)
1133
-
1134
1339
  # Prepare batch data
1135
1340
  label_data = []
1136
1341
  for label in labels:
@@ -1140,15 +1345,14 @@ class MetadataCacheV2:
1140
1345
  label.id,
1141
1346
  label.language,
1142
1347
  label.value,
1143
- expires_at.isoformat(),
1144
1348
  )
1145
1349
  )
1146
1350
 
1147
1351
  async with aiosqlite.connect(self.db_path) as db:
1148
1352
  await db.executemany(
1149
1353
  """INSERT OR REPLACE INTO labels_cache
1150
- (global_version_id, label_id, language, label_text, expires_at, hit_count, last_accessed)
1151
- VALUES (?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP)""",
1354
+ (global_version_id, label_id, language, label_text, hit_count, last_accessed)
1355
+ VALUES (?, ?, ?, ?, 0, CURRENT_TIMESTAMP)""",
1152
1356
  label_data,
1153
1357
  )
1154
1358
  await db.commit()
@@ -1188,15 +1392,13 @@ class MetadataCacheV2:
1188
1392
  params = [global_version_id, language] + label_ids
1189
1393
  query = f"""SELECT label_id, label_text
1190
1394
  FROM labels_cache
1191
- WHERE global_version_id = ? AND language = ? AND label_id IN ({placeholders})
1192
- AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)"""
1395
+ WHERE global_version_id = ? AND language = ? AND label_id IN ({placeholders})"""
1193
1396
  else:
1194
1397
  # Search across all versions (including temporary entries)
1195
1398
  params = [language] + label_ids
1196
1399
  query = f"""SELECT label_id, label_text
1197
1400
  FROM labels_cache
1198
1401
  WHERE language = ? AND label_id IN ({placeholders})
1199
- AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
1200
1402
  ORDER BY global_version_id DESC""" # Prefer actual versions over temporary
1201
1403
 
1202
1404
  cursor = await db.execute(query, params)
@@ -1222,30 +1424,6 @@ class MetadataCacheV2:
1222
1424
  logger.debug(f"Label batch lookup: {len(results)}/{len(label_ids)} found")
1223
1425
  return results
1224
1426
 
1225
- async def clear_expired_labels(self, global_version_id: Optional[int] = None):
1226
- """Clear expired labels from cache
1227
-
1228
- Args:
1229
- global_version_id: Global version ID to clear (clears all if None)
1230
- """
1231
- async with aiosqlite.connect(self.db_path) as db:
1232
- if global_version_id is not None:
1233
- cursor = await db.execute(
1234
- """DELETE FROM labels_cache
1235
- WHERE global_version_id = ? AND expires_at IS NOT NULL AND expires_at <= CURRENT_TIMESTAMP""",
1236
- (global_version_id,),
1237
- )
1238
- else:
1239
- cursor = await db.execute(
1240
- """DELETE FROM labels_cache
1241
- WHERE expires_at IS NOT NULL AND expires_at <= CURRENT_TIMESTAMP"""
1242
- )
1243
-
1244
- deleted_count = cursor.rowcount
1245
- await db.commit()
1246
-
1247
- logger.info(f"Cleared {deleted_count} expired labels")
1248
-
1249
1427
  async def get_label_cache_statistics(
1250
1428
  self, global_version_id: Optional[int] = None
1251
1429
  ) -> Dict[str, Any]:
@@ -1274,23 +1452,10 @@ class MetadataCacheV2:
1274
1452
  )
1275
1453
  stats["total_labels"] = (await cursor.fetchone())[0]
1276
1454
 
1277
- # Expired labels
1278
- cursor = await db.execute(
1279
- f"""SELECT COUNT(*) FROM labels_cache
1280
- {version_filter} {'AND' if version_filter else 'WHERE'}
1281
- expires_at IS NOT NULL AND expires_at <= CURRENT_TIMESTAMP""",
1282
- params,
1283
- )
1284
- stats["expired_labels"] = (await cursor.fetchone())[0]
1285
-
1286
- # Active labels
1287
- stats["active_labels"] = stats["total_labels"] - stats["expired_labels"]
1288
-
1289
1455
  # Languages
1290
1456
  cursor = await db.execute(
1291
1457
  f"""SELECT language, COUNT(*) FROM labels_cache
1292
- {version_filter} {'AND' if version_filter else 'WHERE'}
1293
- (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
1458
+ {version_filter}
1294
1459
  GROUP BY language ORDER BY COUNT(*) DESC""",
1295
1460
  params,
1296
1461
  )
@@ -1330,19 +1495,24 @@ class MetadataCacheV2:
1330
1495
  """Get cache statistics
1331
1496
 
1332
1497
  Returns:
1333
- Dictionary with cache statistics
1498
+ Dictionary with cache statistics scoped to the current environment
1334
1499
  """
1500
+ await self.initialize()
1501
+
1502
+ if self._environment_id is None:
1503
+ raise ValueError("Environment not initialized")
1504
+
1335
1505
  stats = {}
1336
1506
 
1337
- # Database statistics
1338
- db_stats = await self.database.get_database_statistics()
1507
+ # Environment-scoped database statistics
1508
+ db_stats = await self.database.get_environment_database_statistics(self._environment_id)
1339
1509
  stats.update(db_stats)
1340
1510
 
1341
- # Version statistics
1342
- version_stats = await self.version_manager.get_version_statistics()
1511
+ # Environment-scoped version statistics
1512
+ version_stats = await self.version_manager.get_environment_version_statistics(self._environment_id)
1343
1513
  stats["version_manager"] = version_stats
1344
1514
 
1345
- # Current version info
1515
+ # Current version info (already environment-scoped)
1346
1516
  current_version = await self._get_current_global_version_id()
1347
1517
  if current_version:
1348
1518
  version_info = await self.version_manager.get_global_version_info(
@@ -1356,7 +1526,7 @@ class MetadataCacheV2:
1356
1526
  "reference_count": version_info.reference_count,
1357
1527
  }
1358
1528
 
1359
- # Label cache statistics
1529
+ # Label cache statistics (already environment-scoped via current_version)
1360
1530
  label_stats = await self.get_label_cache_statistics(current_version)
1361
1531
  stats["label_cache"] = label_stats
1362
1532