d365fo-client 0.2.1__py3-none-any.whl → 0.2.3__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/auth.py +48 -9
- d365fo_client/client.py +84 -44
- d365fo_client/credential_sources.py +431 -0
- d365fo_client/mcp/client_manager.py +8 -0
- d365fo_client/mcp/main.py +39 -17
- d365fo_client/mcp/models.py +2 -2
- d365fo_client/mcp/server.py +69 -22
- d365fo_client/mcp/tools/__init__.py +2 -0
- d365fo_client/mcp/tools/connection_tools.py +7 -0
- d365fo_client/mcp/tools/profile_tools.py +261 -2
- d365fo_client/mcp/tools/sync_tools.py +503 -0
- d365fo_client/metadata_api.py +68 -1
- d365fo_client/metadata_v2/cache_v2.py +26 -19
- d365fo_client/metadata_v2/database_v2.py +93 -0
- d365fo_client/metadata_v2/global_version_manager.py +62 -4
- d365fo_client/metadata_v2/sync_manager_v2.py +1 -1
- d365fo_client/metadata_v2/sync_session_manager.py +1043 -0
- d365fo_client/models.py +41 -13
- d365fo_client/profile_manager.py +7 -1
- d365fo_client/profiles.py +28 -1
- d365fo_client/sync_models.py +181 -0
- {d365fo_client-0.2.1.dist-info → d365fo_client-0.2.3.dist-info}/METADATA +48 -17
- {d365fo_client-0.2.1.dist-info → d365fo_client-0.2.3.dist-info}/RECORD +28 -24
- {d365fo_client-0.2.1.dist-info → d365fo_client-0.2.3.dist-info}/WHEEL +0 -0
- {d365fo_client-0.2.1.dist-info → d365fo_client-0.2.3.dist-info}/entry_points.txt +0 -0
- {d365fo_client-0.2.1.dist-info → d365fo_client-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {d365fo_client-0.2.1.dist-info → d365fo_client-0.2.3.dist-info}/top_level.txt +0 -0
@@ -213,17 +213,19 @@ class MetadataCacheV2:
|
|
213
213
|
entities: List of data entity information
|
214
214
|
"""
|
215
215
|
async with aiosqlite.connect(self.db_path) as db:
|
216
|
-
|
217
|
-
await db.execute(
|
218
|
-
"DELETE FROM data_entities WHERE global_version_id = ?",
|
219
|
-
(global_version_id,),
|
220
|
-
)
|
216
|
+
|
221
217
|
|
222
218
|
# Insert new entities with label processing
|
223
219
|
for entity in entities:
|
224
220
|
# Process label fallback for this entity
|
225
221
|
processed_label_text = process_label_fallback(entity.label_id, entity.label_text)
|
226
|
-
|
222
|
+
|
223
|
+
# Clear existing entity for this version
|
224
|
+
await db.execute(
|
225
|
+
"DELETE FROM data_entities WHERE global_version_id = ? and name = ?",
|
226
|
+
(global_version_id, entity.name,),
|
227
|
+
)
|
228
|
+
|
227
229
|
await db.execute(
|
228
230
|
"""INSERT INTO data_entities
|
229
231
|
(global_version_id, name, public_entity_name, public_collection_name,
|
@@ -237,7 +239,7 @@ class MetadataCacheV2:
|
|
237
239
|
entity.public_collection_name,
|
238
240
|
entity.label_id,
|
239
241
|
processed_label_text, # Use processed label text
|
240
|
-
entity.entity_category
|
242
|
+
entity.entity_category if entity.entity_category else None,
|
241
243
|
entity.data_service_enabled,
|
242
244
|
entity.data_management_enabled,
|
243
245
|
entity.is_read_only,
|
@@ -245,7 +247,7 @@ class MetadataCacheV2:
|
|
245
247
|
)
|
246
248
|
|
247
249
|
await db.commit()
|
248
|
-
logger.
|
250
|
+
logger.debug(
|
249
251
|
f"Stored {len(entities)} data entities for version {global_version_id}"
|
250
252
|
)
|
251
253
|
|
@@ -481,7 +483,7 @@ class MetadataCacheV2:
|
|
481
483
|
nav_prop.name,
|
482
484
|
nav_prop.related_entity,
|
483
485
|
nav_prop.related_relation_name,
|
484
|
-
nav_prop.cardinality
|
486
|
+
nav_prop.cardinality, # StrEnum automatically converts to string
|
485
487
|
),
|
486
488
|
)
|
487
489
|
|
@@ -519,7 +521,7 @@ class MetadataCacheV2:
|
|
519
521
|
entity_id,
|
520
522
|
global_version_id,
|
521
523
|
action.name,
|
522
|
-
action.binding_kind
|
524
|
+
action.binding_kind, # StrEnum automatically converts to string
|
523
525
|
entity_schema.name,
|
524
526
|
entity_schema.entity_set_name,
|
525
527
|
action.return_type.type_name if action.return_type else None,
|
@@ -1357,7 +1359,7 @@ class MetadataCacheV2:
|
|
1357
1359
|
)
|
1358
1360
|
await db.commit()
|
1359
1361
|
|
1360
|
-
logger.
|
1362
|
+
logger.debug(
|
1361
1363
|
f"Batch cached {len(labels)} labels for version {global_version_id}"
|
1362
1364
|
)
|
1363
1365
|
|
@@ -1495,19 +1497,24 @@ class MetadataCacheV2:
|
|
1495
1497
|
"""Get cache statistics
|
1496
1498
|
|
1497
1499
|
Returns:
|
1498
|
-
Dictionary with cache statistics
|
1500
|
+
Dictionary with cache statistics scoped to the current environment
|
1499
1501
|
"""
|
1502
|
+
await self.initialize()
|
1503
|
+
|
1504
|
+
if self._environment_id is None:
|
1505
|
+
raise ValueError("Environment not initialized")
|
1506
|
+
|
1500
1507
|
stats = {}
|
1501
1508
|
|
1502
|
-
#
|
1503
|
-
db_stats = await self.database.
|
1509
|
+
# Environment-scoped database statistics
|
1510
|
+
db_stats = await self.database.get_environment_database_statistics(self._environment_id)
|
1504
1511
|
stats.update(db_stats)
|
1505
1512
|
|
1506
|
-
#
|
1507
|
-
version_stats = await self.version_manager.
|
1513
|
+
# Environment-scoped version statistics
|
1514
|
+
version_stats = await self.version_manager.get_environment_version_statistics(self._environment_id)
|
1508
1515
|
stats["version_manager"] = version_stats
|
1509
1516
|
|
1510
|
-
# Current version info
|
1517
|
+
# Current version info (already environment-scoped)
|
1511
1518
|
current_version = await self._get_current_global_version_id()
|
1512
1519
|
if current_version:
|
1513
1520
|
version_info = await self.version_manager.get_global_version_info(
|
@@ -1517,11 +1524,11 @@ class MetadataCacheV2:
|
|
1517
1524
|
stats["current_version"] = {
|
1518
1525
|
"global_version_id": version_info.id,
|
1519
1526
|
"version_hash": version_info.version_hash,
|
1520
|
-
"modules_count": len(version_info.
|
1527
|
+
"modules_count": len(version_info.modules),
|
1521
1528
|
"reference_count": version_info.reference_count,
|
1522
1529
|
}
|
1523
1530
|
|
1524
|
-
# Label cache statistics
|
1531
|
+
# Label cache statistics (already environment-scoped via current_version)
|
1525
1532
|
label_stats = await self.get_label_cache_statistics(current_version)
|
1526
1533
|
stats["label_cache"] = label_stats
|
1527
1534
|
|
@@ -542,6 +542,99 @@ class MetadataDatabaseV2:
|
|
542
542
|
|
543
543
|
return stats
|
544
544
|
|
545
|
+
async def get_statistics(self) -> Dict[str, Any]:
|
546
|
+
"""Alias for get_database_statistics for backward compatibility
|
547
|
+
|
548
|
+
Returns:
|
549
|
+
Dictionary with database statistics
|
550
|
+
"""
|
551
|
+
return await self.get_database_statistics()
|
552
|
+
|
553
|
+
async def get_environment_database_statistics(self, environment_id: int) -> Dict[str, Any]:
|
554
|
+
"""Get database statistics scoped to a specific environment
|
555
|
+
|
556
|
+
Args:
|
557
|
+
environment_id: Environment ID to get statistics for
|
558
|
+
|
559
|
+
Returns:
|
560
|
+
Dictionary with environment-scoped database statistics
|
561
|
+
"""
|
562
|
+
async with aiosqlite.connect(self.db_path) as db:
|
563
|
+
stats = {}
|
564
|
+
|
565
|
+
# Get active global versions for this environment
|
566
|
+
cursor = await db.execute(
|
567
|
+
"""SELECT DISTINCT global_version_id
|
568
|
+
FROM environment_versions
|
569
|
+
WHERE environment_id = ? AND is_active = 1""",
|
570
|
+
(environment_id,)
|
571
|
+
)
|
572
|
+
active_versions = [row[0] for row in await cursor.fetchall()]
|
573
|
+
|
574
|
+
if not active_versions:
|
575
|
+
# No active versions, return zero counts
|
576
|
+
return {
|
577
|
+
"data_entities_count": 0,
|
578
|
+
"public_entities_count": 0,
|
579
|
+
"entity_properties_count": 0,
|
580
|
+
"navigation_properties_count": 0,
|
581
|
+
"entity_actions_count": 0,
|
582
|
+
"enumerations_count": 0,
|
583
|
+
"labels_cache_count": 0,
|
584
|
+
"environment_statistics": {
|
585
|
+
"total_environments": 1,
|
586
|
+
"linked_versions": 0,
|
587
|
+
},
|
588
|
+
"database_size_bytes": None,
|
589
|
+
"database_size_mb": None,
|
590
|
+
}
|
591
|
+
|
592
|
+
# Create placeholders for SQL IN clause
|
593
|
+
version_placeholders = ",".join("?" for _ in active_versions)
|
594
|
+
|
595
|
+
# Environment-scoped metadata counts
|
596
|
+
tables = [
|
597
|
+
("data_entities", "entities"),
|
598
|
+
("public_entities", "public_entities"),
|
599
|
+
("entity_properties", "properties"),
|
600
|
+
("navigation_properties", "navigation_properties"),
|
601
|
+
("entity_actions", "actions"),
|
602
|
+
("enumerations", "enumerations"),
|
603
|
+
("labels_cache", "labels"),
|
604
|
+
]
|
605
|
+
|
606
|
+
for table, key in tables:
|
607
|
+
cursor = await db.execute(
|
608
|
+
f"SELECT COUNT(*) FROM {table} WHERE global_version_id IN ({version_placeholders})",
|
609
|
+
active_versions
|
610
|
+
)
|
611
|
+
stats[f"{table}_count"] = (await cursor.fetchone())[0]
|
612
|
+
|
613
|
+
# Environment-specific statistics
|
614
|
+
cursor = await db.execute(
|
615
|
+
"""SELECT
|
616
|
+
COUNT(DISTINCT ev.global_version_id) as linked_versions
|
617
|
+
FROM environment_versions ev
|
618
|
+
WHERE ev.environment_id = ? AND ev.is_active = 1""",
|
619
|
+
(environment_id,)
|
620
|
+
)
|
621
|
+
env_stats = await cursor.fetchone()
|
622
|
+
stats["environment_statistics"] = {
|
623
|
+
"total_environments": 1, # Current environment only
|
624
|
+
"linked_versions": env_stats[0] or 0,
|
625
|
+
}
|
626
|
+
|
627
|
+
# Database file size (shared across all environments)
|
628
|
+
try:
|
629
|
+
db_size = self.db_path.stat().st_size
|
630
|
+
stats["database_size_bytes"] = db_size
|
631
|
+
stats["database_size_mb"] = round(db_size / (1024 * 1024), 2)
|
632
|
+
except Exception:
|
633
|
+
stats["database_size_bytes"] = None
|
634
|
+
stats["database_size_mb"] = None
|
635
|
+
|
636
|
+
return stats
|
637
|
+
|
545
638
|
async def vacuum_database(self) -> bool:
|
546
639
|
"""Vacuum database to reclaim space
|
547
640
|
|
@@ -325,13 +325,11 @@ class GlobalVersionManager:
|
|
325
325
|
first_seen_at=datetime.fromisoformat(row[3]),
|
326
326
|
last_used_at=datetime.fromisoformat(row[4]),
|
327
327
|
reference_count=row[5],
|
328
|
-
|
329
|
-
modules[:10] if modules else []
|
330
|
-
), # Use first 10 modules as sample
|
328
|
+
modules=modules
|
331
329
|
)
|
332
330
|
|
333
331
|
async def find_compatible_versions(
|
334
|
-
self, modules: List[ModuleVersionInfo], exact_match: bool =
|
332
|
+
self, modules: List[ModuleVersionInfo], exact_match: bool = True
|
335
333
|
) -> List[GlobalVersionInfo]:
|
336
334
|
"""Find compatible global versions
|
337
335
|
|
@@ -571,3 +569,63 @@ class GlobalVersionManager:
|
|
571
569
|
}
|
572
570
|
|
573
571
|
return stats
|
572
|
+
|
573
|
+
async def get_environment_version_statistics(self, environment_id: int) -> Dict[str, Any]:
|
574
|
+
"""Get version statistics scoped to a specific environment
|
575
|
+
|
576
|
+
Args:
|
577
|
+
environment_id: Environment ID to get statistics for
|
578
|
+
|
579
|
+
Returns:
|
580
|
+
Dictionary with environment-scoped version statistics
|
581
|
+
"""
|
582
|
+
async with aiosqlite.connect(self.db_path) as db:
|
583
|
+
stats = {}
|
584
|
+
|
585
|
+
# Get versions for this specific environment
|
586
|
+
cursor = await db.execute(
|
587
|
+
"""SELECT COUNT(DISTINCT global_version_id)
|
588
|
+
FROM environment_versions
|
589
|
+
WHERE environment_id = ? AND is_active = 1""",
|
590
|
+
(environment_id,)
|
591
|
+
)
|
592
|
+
stats["total_versions"] = (await cursor.fetchone())[0]
|
593
|
+
|
594
|
+
# This environment only
|
595
|
+
stats["total_environments"] = 1
|
596
|
+
|
597
|
+
# Reference statistics for this environment's versions
|
598
|
+
cursor = await db.execute(
|
599
|
+
"""SELECT
|
600
|
+
SUM(gv.reference_count) as total_references,
|
601
|
+
AVG(gv.reference_count) as avg_references,
|
602
|
+
MAX(gv.reference_count) as max_references,
|
603
|
+
COUNT(*) as versions_with_refs
|
604
|
+
FROM global_versions gv
|
605
|
+
INNER JOIN environment_versions ev ON gv.id = ev.global_version_id
|
606
|
+
WHERE ev.environment_id = ? AND ev.is_active = 1 AND gv.reference_count > 0""",
|
607
|
+
(environment_id,)
|
608
|
+
)
|
609
|
+
ref_stats = await cursor.fetchone()
|
610
|
+
stats["reference_statistics"] = {
|
611
|
+
"total_references": ref_stats[0] or 0,
|
612
|
+
"avg_references": round(ref_stats[1] or 0, 2),
|
613
|
+
"max_references": ref_stats[2] or 0,
|
614
|
+
"versions_with_references": ref_stats[3] or 0,
|
615
|
+
}
|
616
|
+
|
617
|
+
# Version age statistics for this environment
|
618
|
+
cursor = await db.execute(
|
619
|
+
"""SELECT
|
620
|
+
COUNT(*) as recent_versions
|
621
|
+
FROM global_versions gv
|
622
|
+
INNER JOIN environment_versions ev ON gv.id = ev.global_version_id
|
623
|
+
WHERE ev.environment_id = ? AND ev.is_active = 1
|
624
|
+
AND gv.last_used_at >= datetime('now', '-7 days')""",
|
625
|
+
(environment_id,)
|
626
|
+
)
|
627
|
+
stats["recent_activity"] = {
|
628
|
+
"versions_used_last_7_days": (await cursor.fetchone())[0]
|
629
|
+
}
|
630
|
+
|
631
|
+
return stats
|
@@ -656,7 +656,7 @@ class SmartSyncManagerV2:
|
|
656
656
|
|
657
657
|
# Check for compatible versions (sharing opportunity)
|
658
658
|
compatible_versions = await self.version_manager.find_compatible_versions(
|
659
|
-
version_info.
|
659
|
+
version_info.modules, exact_match=True
|
660
660
|
)
|
661
661
|
|
662
662
|
for version in compatible_versions:
|