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.
- d365fo_client/__init__.py +4 -48
- d365fo_client/client.py +102 -56
- d365fo_client/config.py +8 -0
- d365fo_client/labels.py +3 -3
- d365fo_client/main.py +6 -6
- d365fo_client/mcp/client_manager.py +30 -2
- d365fo_client/mcp/main.py +20 -7
- d365fo_client/mcp/models.py +2 -2
- d365fo_client/mcp/server.py +90 -5
- d365fo_client/mcp/tools/connection_tools.py +7 -0
- d365fo_client/mcp/tools/profile_tools.py +15 -0
- d365fo_client/metadata_api.py +223 -6
- d365fo_client/metadata_v2/cache_v2.py +267 -97
- d365fo_client/metadata_v2/database_v2.py +93 -2
- d365fo_client/metadata_v2/global_version_manager.py +60 -0
- d365fo_client/metadata_v2/label_utils.py +107 -0
- d365fo_client/metadata_v2/sync_manager_v2.py +83 -9
- d365fo_client/models.py +19 -10
- d365fo_client/output.py +4 -4
- d365fo_client/profile_manager.py +12 -0
- {d365fo_client-0.1.0.dist-info → d365fo_client-0.2.2.dist-info}/METADATA +5 -1
- {d365fo_client-0.1.0.dist-info → d365fo_client-0.2.2.dist-info}/RECORD +26 -25
- {d365fo_client-0.1.0.dist-info → d365fo_client-0.2.2.dist-info}/WHEEL +0 -0
- {d365fo_client-0.1.0.dist-info → d365fo_client-0.2.2.dist-info}/entry_points.txt +0 -0
- {d365fo_client-0.1.0.dist-info → d365fo_client-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {d365fo_client-0.1.0.dist-info → d365fo_client-0.2.2.dist-info}/top_level.txt +0 -0
@@ -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
|
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
|
-
|
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=
|
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
|
-
|
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
|
-
|
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
|
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
|
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=
|
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=
|
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
|
-
|
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
|
-
|
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=
|
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=
|
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
|
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
|
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
|
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,
|
1091
|
-
VALUES (?, ?, ?, ?,
|
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,
|
1151
|
-
VALUES (?, ?, ?, ?,
|
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}
|
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
|
-
#
|
1338
|
-
db_stats = await self.database.
|
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
|
-
#
|
1342
|
-
version_stats = await self.version_manager.
|
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
|
|