d365fo-client 0.2.2__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/auth.py +48 -9
- d365fo_client/client.py +40 -20
- 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/server.py +69 -22
- d365fo_client/mcp/tools/__init__.py +2 -0
- d365fo_client/mcp/tools/profile_tools.py +261 -2
- d365fo_client/mcp/tools/sync_tools.py +503 -0
- d365fo_client/metadata_api.py +67 -0
- d365fo_client/metadata_v2/cache_v2.py +11 -9
- d365fo_client/metadata_v2/global_version_manager.py +2 -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 +22 -3
- d365fo_client/profile_manager.py +7 -1
- d365fo_client/profiles.py +28 -1
- d365fo_client/sync_models.py +181 -0
- {d365fo_client-0.2.2.dist-info → d365fo_client-0.2.3.dist-info}/METADATA +48 -17
- {d365fo_client-0.2.2.dist-info → d365fo_client-0.2.3.dist-info}/RECORD +24 -20
- {d365fo_client-0.2.2.dist-info → d365fo_client-0.2.3.dist-info}/WHEEL +0 -0
- {d365fo_client-0.2.2.dist-info → d365fo_client-0.2.3.dist-info}/entry_points.txt +0 -0
- {d365fo_client-0.2.2.dist-info → d365fo_client-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {d365fo_client-0.2.2.dist-info → d365fo_client-0.2.3.dist-info}/top_level.txt +0 -0
d365fo_client/models.py
CHANGED
@@ -12,6 +12,7 @@ from .utils import get_environment_cache_directory
|
|
12
12
|
|
13
13
|
if TYPE_CHECKING:
|
14
14
|
from typing import ForwardRef
|
15
|
+
from .credential_sources import CredentialSource
|
15
16
|
|
16
17
|
|
17
18
|
def _ensure_str_for_json(field):
|
@@ -65,7 +66,10 @@ class SyncStrategy(StrEnum):
|
|
65
66
|
FULL = "full"
|
66
67
|
INCREMENTAL = "incremental"
|
67
68
|
ENTITIES_ONLY = "entities_only"
|
69
|
+
LABELS_ONLY = "labels_only"
|
68
70
|
SHARING_MODE = "sharing_mode"
|
71
|
+
FULL_WITHOUT_LABELS = "full_without_labels"
|
72
|
+
|
69
73
|
|
70
74
|
|
71
75
|
class Cardinality(StrEnum):
|
@@ -80,6 +84,7 @@ class FOClientConfig:
|
|
80
84
|
"""Configuration for F&O Client"""
|
81
85
|
|
82
86
|
base_url: str
|
87
|
+
auth_mode: Optional[str] = "defaut" # default | client_credentials
|
83
88
|
client_id: Optional[str] = None
|
84
89
|
client_secret: Optional[str] = None
|
85
90
|
tenant_id: Optional[str] = None
|
@@ -98,6 +103,8 @@ class FOClientConfig:
|
|
98
103
|
max_memory_cache_size: int = 1000
|
99
104
|
# Cache-first behavior configuration
|
100
105
|
use_cache_first: bool = True
|
106
|
+
# Credential source configuration
|
107
|
+
credential_source: Optional["CredentialSource"] = None
|
101
108
|
|
102
109
|
def __post_init__(self):
|
103
110
|
"""Post-initialization to set default cache directory if not provided."""
|
@@ -738,9 +745,9 @@ class GlobalVersionInfo:
|
|
738
745
|
first_seen_at: datetime
|
739
746
|
last_used_at: datetime
|
740
747
|
reference_count: int
|
741
|
-
|
748
|
+
modules: List[ModuleVersionInfo] = field(
|
742
749
|
default_factory=list
|
743
|
-
) #
|
750
|
+
) # Modules for debugging
|
744
751
|
|
745
752
|
def to_dict(self) -> Dict[str, Any]:
|
746
753
|
"""Convert to dictionary for JSON serialization"""
|
@@ -751,7 +758,7 @@ class GlobalVersionInfo:
|
|
751
758
|
"first_seen_at": self.first_seen_at.isoformat(),
|
752
759
|
"last_used_at": self.last_used_at.isoformat(),
|
753
760
|
"reference_count": self.reference_count,
|
754
|
-
"
|
761
|
+
"modules": [module.to_dict() for module in self.modules],
|
755
762
|
}
|
756
763
|
|
757
764
|
|
@@ -869,3 +876,15 @@ class SyncResult:
|
|
869
876
|
action_count: int
|
870
877
|
enumeration_count: int
|
871
878
|
label_count: int
|
879
|
+
|
880
|
+
def to_dict(self) -> dict:
|
881
|
+
"""Convert to dictionary for JSON serialization"""
|
882
|
+
return {
|
883
|
+
"success": self.success,
|
884
|
+
"error": self.error,
|
885
|
+
"duration_ms": self.duration_ms,
|
886
|
+
"entity_count": self.entity_count,
|
887
|
+
"action_count": self.action_count,
|
888
|
+
"enumeration_count": self.enumeration_count,
|
889
|
+
"label_count": self.label_count
|
890
|
+
}
|
d365fo_client/profile_manager.py
CHANGED
@@ -7,13 +7,15 @@ import logging
|
|
7
7
|
import os
|
8
8
|
from dataclasses import asdict
|
9
9
|
from pathlib import Path
|
10
|
-
from typing import Any, Dict, List, Optional
|
10
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
11
11
|
|
12
12
|
import yaml
|
13
13
|
|
14
14
|
from .config import ConfigManager
|
15
15
|
from .models import FOClientConfig
|
16
16
|
from .profiles import Profile
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from .credential_sources import CredentialSource
|
17
19
|
|
18
20
|
logger = logging.getLogger(__name__)
|
19
21
|
|
@@ -76,6 +78,8 @@ class ProfileManager:
|
|
76
78
|
language: str = "en-US",
|
77
79
|
cache_dir: Optional[str] = None,
|
78
80
|
description: Optional[str] = None,
|
81
|
+
credential_source: Optional["CredentialSource"] = None
|
82
|
+
|
79
83
|
) -> bool:
|
80
84
|
"""Create a new profile.
|
81
85
|
|
@@ -93,6 +97,7 @@ class ProfileManager:
|
|
93
97
|
language: Default language code
|
94
98
|
cache_dir: Cache directory path
|
95
99
|
description: Profile description (stored separately from CLI profile)
|
100
|
+
credential_source: Credential source configuration
|
96
101
|
|
97
102
|
Returns:
|
98
103
|
True if created successfully
|
@@ -120,6 +125,7 @@ class ProfileManager:
|
|
120
125
|
cache_dir=cache_dir,
|
121
126
|
description=description,
|
122
127
|
output_format="table", # Default for CLI compatibility
|
128
|
+
credential_source=credential_source
|
123
129
|
)
|
124
130
|
|
125
131
|
self.config_manager.save_profile(profile)
|
d365fo_client/profiles.py
CHANGED
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
6
6
|
|
7
7
|
if TYPE_CHECKING:
|
8
8
|
from .models import FOClientConfig
|
9
|
+
from .credential_sources import CredentialSource
|
9
10
|
|
10
11
|
logger = logging.getLogger(__name__)
|
11
12
|
|
@@ -24,8 +25,10 @@ class Profile:
|
|
24
25
|
client_id: Optional[str] = None
|
25
26
|
client_secret: Optional[str] = None
|
26
27
|
tenant_id: Optional[str] = None
|
28
|
+
use_default_credentials: Optional[bool] = None # None means derive from auth_mode
|
27
29
|
verify_ssl: bool = True
|
28
30
|
timeout: int = 60
|
31
|
+
credential_source: Optional["CredentialSource"] = None
|
29
32
|
|
30
33
|
# Cache settings
|
31
34
|
use_label_cache: bool = True
|
@@ -43,18 +46,26 @@ class Profile:
|
|
43
46
|
"""Convert profile to FOClientConfig."""
|
44
47
|
from .models import FOClientConfig
|
45
48
|
|
49
|
+
# Determine use_default_credentials: explicit setting takes precedence over auth_mode
|
50
|
+
if self.use_default_credentials is not None:
|
51
|
+
use_default_creds = self.use_default_credentials
|
52
|
+
else:
|
53
|
+
use_default_creds = self.auth_mode == "default"
|
54
|
+
|
46
55
|
return FOClientConfig(
|
47
56
|
base_url=self.base_url,
|
57
|
+
auth_mode=self.auth_mode,
|
48
58
|
client_id=self.client_id,
|
49
59
|
client_secret=self.client_secret,
|
50
60
|
tenant_id=self.tenant_id,
|
51
|
-
use_default_credentials=
|
61
|
+
use_default_credentials=use_default_creds,
|
52
62
|
timeout=self.timeout,
|
53
63
|
verify_ssl=self.verify_ssl,
|
54
64
|
use_label_cache=self.use_label_cache,
|
55
65
|
label_cache_expiry_minutes=self.label_cache_expiry_minutes,
|
56
66
|
use_cache_first=self.use_cache_first,
|
57
67
|
metadata_cache_dir=self.cache_dir,
|
68
|
+
credential_source=self.credential_source,
|
58
69
|
)
|
59
70
|
|
60
71
|
def validate(self) -> List[str]:
|
@@ -100,6 +111,7 @@ class Profile:
|
|
100
111
|
"client_id": None,
|
101
112
|
"client_secret": None,
|
102
113
|
"tenant_id": None,
|
114
|
+
"use_default_credentials": None,
|
103
115
|
"verify_ssl": True,
|
104
116
|
"timeout": 60,
|
105
117
|
"use_label_cache": True,
|
@@ -108,6 +120,7 @@ class Profile:
|
|
108
120
|
"cache_dir": None,
|
109
121
|
"language": "en-US",
|
110
122
|
"output_format": "table",
|
123
|
+
"credential_source": None,
|
111
124
|
}
|
112
125
|
|
113
126
|
for key, default_value in defaults.items():
|
@@ -119,6 +132,16 @@ class Profile:
|
|
119
132
|
k: v for k, v in migrated_data.items() if k in cls.__dataclass_fields__
|
120
133
|
}
|
121
134
|
|
135
|
+
# Handle credential_source deserialization
|
136
|
+
if "credential_source" in valid_params and valid_params["credential_source"] is not None:
|
137
|
+
from .credential_sources import CredentialSource
|
138
|
+
credential_source_data = valid_params["credential_source"]
|
139
|
+
try:
|
140
|
+
valid_params["credential_source"] = CredentialSource.from_dict(credential_source_data)
|
141
|
+
except Exception as e:
|
142
|
+
logger.error(f"Error deserializing credential_source: {e}")
|
143
|
+
valid_params["credential_source"] = None
|
144
|
+
|
122
145
|
try:
|
123
146
|
return cls(**valid_params)
|
124
147
|
except Exception as e:
|
@@ -151,6 +174,10 @@ class Profile:
|
|
151
174
|
data = asdict(self)
|
152
175
|
data.pop("name", None)
|
153
176
|
|
177
|
+
# Handle credential_source serialization
|
178
|
+
if self.credential_source is not None:
|
179
|
+
data["credential_source"] = self.credential_source.to_dict()
|
180
|
+
|
154
181
|
return data
|
155
182
|
|
156
183
|
def clone(self, name: str, **overrides) -> "Profile":
|
@@ -0,0 +1,181 @@
|
|
1
|
+
"""Enhanced sync models for progress reporting and session management."""
|
2
|
+
|
3
|
+
import uuid
|
4
|
+
from datetime import datetime, timezone
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from enum import StrEnum
|
7
|
+
from typing import Dict, List, Optional, Callable, Set
|
8
|
+
|
9
|
+
from .models import SyncStrategy, SyncResult
|
10
|
+
|
11
|
+
|
12
|
+
class SyncStatus(StrEnum):
|
13
|
+
"""Sync operation status"""
|
14
|
+
PENDING = "pending"
|
15
|
+
RUNNING = "running"
|
16
|
+
COMPLETED = "completed"
|
17
|
+
FAILED = "failed"
|
18
|
+
CANCELLED = "cancelled"
|
19
|
+
PAUSED = "paused"
|
20
|
+
|
21
|
+
|
22
|
+
class SyncPhase(StrEnum):
|
23
|
+
"""Detailed sync phases"""
|
24
|
+
INITIALIZING = "initializing"
|
25
|
+
VERSION_CHECK = "version_check"
|
26
|
+
ENTITIES = "entities"
|
27
|
+
SCHEMAS = "schemas"
|
28
|
+
ENUMERATIONS = "enumerations"
|
29
|
+
LABELS = "labels"
|
30
|
+
INDEXING = "indexing"
|
31
|
+
FINALIZING = "finalizing"
|
32
|
+
COMPLETED = "completed"
|
33
|
+
FAILED = "failed"
|
34
|
+
|
35
|
+
|
36
|
+
@dataclass
|
37
|
+
class SyncActivity:
|
38
|
+
"""Individual sync activity within a phase"""
|
39
|
+
name: str
|
40
|
+
status: SyncStatus
|
41
|
+
start_time: Optional[datetime] = None
|
42
|
+
end_time: Optional[datetime] = None
|
43
|
+
progress_percent: float = 0.0
|
44
|
+
items_processed: int = 0
|
45
|
+
items_total: Optional[int] = None
|
46
|
+
current_item: Optional[str] = None
|
47
|
+
error: Optional[str] = None
|
48
|
+
|
49
|
+
def to_dict(self) -> dict:
|
50
|
+
"""Convert to dictionary for JSON serialization"""
|
51
|
+
return {
|
52
|
+
"name": self.name,
|
53
|
+
"status": self.status,
|
54
|
+
"start_time": self.start_time.isoformat() if self.start_time else None,
|
55
|
+
"end_time": self.end_time.isoformat() if self.end_time else None,
|
56
|
+
"progress_percent": self.progress_percent,
|
57
|
+
"items_processed": self.items_processed,
|
58
|
+
"items_total": self.items_total,
|
59
|
+
"current_item": self.current_item,
|
60
|
+
"error": self.error
|
61
|
+
}
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass
|
65
|
+
class SyncSession:
|
66
|
+
"""Complete sync session with detailed tracking"""
|
67
|
+
session_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
68
|
+
global_version_id: int = 0
|
69
|
+
strategy: SyncStrategy = SyncStrategy.FULL
|
70
|
+
status: SyncStatus = SyncStatus.PENDING
|
71
|
+
|
72
|
+
# Overall progress
|
73
|
+
start_time: Optional[datetime] = None
|
74
|
+
end_time: Optional[datetime] = None
|
75
|
+
estimated_completion: Optional[datetime] = None
|
76
|
+
progress_percent: float = 0.0
|
77
|
+
|
78
|
+
# Current state
|
79
|
+
current_phase: SyncPhase = SyncPhase.INITIALIZING
|
80
|
+
current_activity: Optional[str] = None
|
81
|
+
|
82
|
+
# Phase tracking
|
83
|
+
phases: Dict[SyncPhase, SyncActivity] = field(default_factory=dict)
|
84
|
+
|
85
|
+
# Results
|
86
|
+
result: Optional[SyncResult] = None
|
87
|
+
error: Optional[str] = None
|
88
|
+
|
89
|
+
# Metadata
|
90
|
+
initiated_by: str = "system" # user, system, scheduled, mcp
|
91
|
+
can_cancel: bool = True
|
92
|
+
|
93
|
+
# Collected label IDs during sync for efficient label processing
|
94
|
+
collected_label_ids: Set[str] = field(default_factory=set)
|
95
|
+
|
96
|
+
def get_overall_progress(self) -> float:
|
97
|
+
"""Calculate overall progress across all phases"""
|
98
|
+
if not self.phases:
|
99
|
+
return 0.0
|
100
|
+
|
101
|
+
total_weight = len(self.phases)
|
102
|
+
completed_weight = sum(
|
103
|
+
1.0 if activity.status == SyncStatus.COMPLETED
|
104
|
+
else activity.progress_percent / 100.0
|
105
|
+
for activity in self.phases.values()
|
106
|
+
)
|
107
|
+
return min(100.0, (completed_weight / total_weight) * 100.0)
|
108
|
+
|
109
|
+
def get_current_activity_detail(self) -> Optional[SyncActivity]:
|
110
|
+
"""Get current running activity details"""
|
111
|
+
if self.current_phase in self.phases:
|
112
|
+
return self.phases[self.current_phase]
|
113
|
+
return None
|
114
|
+
|
115
|
+
def estimate_remaining_time(self) -> Optional[int]:
|
116
|
+
"""Estimate remaining time in seconds"""
|
117
|
+
if not self.start_time or self.progress_percent <= 0:
|
118
|
+
return None
|
119
|
+
|
120
|
+
elapsed = (datetime.now(timezone.utc) - self.start_time).total_seconds()
|
121
|
+
if self.progress_percent >= 100:
|
122
|
+
return 0
|
123
|
+
|
124
|
+
estimated_total = elapsed / (self.progress_percent / 100.0)
|
125
|
+
return max(0, int(estimated_total - elapsed))
|
126
|
+
|
127
|
+
def to_dict(self) -> dict:
|
128
|
+
"""Convert to dictionary for JSON serialization"""
|
129
|
+
return {
|
130
|
+
"session_id": self.session_id,
|
131
|
+
"global_version_id": self.global_version_id,
|
132
|
+
"strategy": self.strategy,
|
133
|
+
"status": self.status,
|
134
|
+
"start_time": self.start_time.isoformat() if self.start_time else None,
|
135
|
+
"end_time": self.end_time.isoformat() if self.end_time else None,
|
136
|
+
"estimated_completion": self.estimated_completion.isoformat() if self.estimated_completion else None,
|
137
|
+
"progress_percent": self.progress_percent,
|
138
|
+
"current_phase": self.current_phase,
|
139
|
+
"current_activity": self.current_activity,
|
140
|
+
"phases": {
|
141
|
+
phase.value: activity.to_dict()
|
142
|
+
for phase, activity in self.phases.items()
|
143
|
+
},
|
144
|
+
"result": self.result.to_dict() if self.result and hasattr(self.result, 'to_dict') else None,
|
145
|
+
"error": self.error,
|
146
|
+
"initiated_by": self.initiated_by,
|
147
|
+
"can_cancel": self.can_cancel,
|
148
|
+
"estimated_remaining_seconds": self.estimate_remaining_time()
|
149
|
+
}
|
150
|
+
|
151
|
+
|
152
|
+
@dataclass
|
153
|
+
class SyncSessionSummary:
|
154
|
+
"""Lightweight sync session summary for listing"""
|
155
|
+
session_id: str
|
156
|
+
global_version_id: int
|
157
|
+
strategy: SyncStrategy
|
158
|
+
status: SyncStatus
|
159
|
+
start_time: Optional[datetime] = None
|
160
|
+
end_time: Optional[datetime] = None
|
161
|
+
progress_percent: float = 0.0
|
162
|
+
current_phase: SyncPhase = SyncPhase.INITIALIZING
|
163
|
+
current_activity: Optional[str] = None
|
164
|
+
initiated_by: str = "system"
|
165
|
+
duration_seconds: Optional[int] = None
|
166
|
+
|
167
|
+
def to_dict(self) -> dict:
|
168
|
+
"""Convert to dictionary for JSON serialization"""
|
169
|
+
return {
|
170
|
+
"session_id": self.session_id,
|
171
|
+
"global_version_id": self.global_version_id,
|
172
|
+
"strategy": self.strategy,
|
173
|
+
"status": self.status,
|
174
|
+
"start_time": self.start_time.isoformat() if self.start_time else None,
|
175
|
+
"end_time": self.end_time.isoformat() if self.end_time else None,
|
176
|
+
"progress_percent": self.progress_percent,
|
177
|
+
"current_phase": self.current_phase,
|
178
|
+
"current_activity": self.current_activity,
|
179
|
+
"initiated_by": self.initiated_by,
|
180
|
+
"duration_seconds": self.duration_seconds
|
181
|
+
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: d365fo-client
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.3
|
4
4
|
Summary: Microsoft Dynamics 365 Finance & Operations client
|
5
5
|
Author-email: Muhammad Afzaal <mo@thedataguy.pro>
|
6
6
|
License-Expression: MIT
|
@@ -22,6 +22,7 @@ License-File: LICENSE
|
|
22
22
|
Requires-Dist: aiohttp>=3.10.0
|
23
23
|
Requires-Dist: aiofiles>=24.1.0
|
24
24
|
Requires-Dist: azure-identity>=1.19.0
|
25
|
+
Requires-Dist: azure-keyvault-secrets>=4.8.0
|
25
26
|
Requires-Dist: requests>=2.32.0
|
26
27
|
Requires-Dist: aiosqlite>=0.19.0
|
27
28
|
Requires-Dist: cachetools>=6.1.0
|
@@ -50,7 +51,7 @@ A comprehensive Python client library and MCP server for Microsoft Dynamics 365
|
|
50
51
|
- 🏷️ **Label Operations V2**: Multilingual label caching with performance improvements and async support
|
51
52
|
- 🔍 **Advanced Querying**: Support for all OData query parameters ($select, $filter, $expand, etc.)
|
52
53
|
- ⚡ **Action Execution**: Execute bound and unbound OData actions with comprehensive parameter handling
|
53
|
-
- 🔒 **Authentication**: Azure AD integration with default credentials
|
54
|
+
- 🔒 **Authentication**: Azure AD integration with default credentials, service principal, and Azure Key Vault support
|
54
55
|
- 💾 **Intelligent Caching**: Cross-environment cache sharing with module-based version detection
|
55
56
|
- 🌐 **Async/Await**: Modern async/await patterns with optimized session management
|
56
57
|
- 📝 **Type Hints**: Full type annotation support with enhanced data models
|
@@ -58,6 +59,8 @@ A comprehensive Python client library and MCP server for Microsoft Dynamics 365
|
|
58
59
|
- 🖥️ **Comprehensive CLI**: Hierarchical command-line interface for all D365 F&O operations
|
59
60
|
- 🧪 **Multi-tier Testing**: Mock, sandbox, and live integration testing framework (17/17 tests passing)
|
60
61
|
- 📋 **Metadata Scripts**: PowerShell and Python utilities for entity, enumeration, and action discovery
|
62
|
+
- 🔐 **Enhanced Credential Management**: Support for Azure Key Vault and multiple credential sources
|
63
|
+
- 📊 **Advanced Sync Management**: Session-based synchronization with detailed progress tracking
|
61
64
|
|
62
65
|
## Installation
|
63
66
|
|
@@ -73,6 +76,13 @@ uv sync # Installs with exact dependencies from uv.lock
|
|
73
76
|
|
74
77
|
**Note**: The package includes MCP (Model Context Protocol) dependencies by default, enabling AI assistant integration. Both `d365fo-client` CLI and `d365fo-mcp-server` commands will be available after installation.
|
75
78
|
|
79
|
+
**Breaking Change in v0.2.3**: Environment variable names have been updated for consistency:
|
80
|
+
- `AZURE_CLIENT_ID` → `D365FO_CLIENT_ID`
|
81
|
+
- `AZURE_CLIENT_SECRET` → `D365FO_CLIENT_SECRET`
|
82
|
+
- `AZURE_TENANT_ID` → `D365FO_TENANT_ID`
|
83
|
+
|
84
|
+
Please update your environment variables accordingly when upgrading.
|
85
|
+
|
76
86
|
## Quick Start
|
77
87
|
|
78
88
|
## Command Line Interface (CLI)
|
@@ -158,9 +168,9 @@ profiles:
|
|
158
168
|
|
159
169
|
development:
|
160
170
|
base_url: "https://dev.dynamics.com"
|
161
|
-
client_id: "${
|
162
|
-
client_secret: "${
|
163
|
-
tenant_id: "${
|
171
|
+
client_id: "${D365FO_CLIENT_ID}"
|
172
|
+
client_secret: "${D365FO_CLIENT_SECRET}"
|
173
|
+
tenant_id: "${D365FO_TENANT_ID}"
|
164
174
|
use_cache_first: true
|
165
175
|
|
166
176
|
default_profile: "development"
|
@@ -262,7 +272,14 @@ config = FOClientConfig(
|
|
262
272
|
use_default_credentials=False
|
263
273
|
)
|
264
274
|
|
265
|
-
# Option 3:
|
275
|
+
# Option 3: Azure Key Vault integration (New in v0.2.3)
|
276
|
+
config = FOClientConfig(
|
277
|
+
base_url="https://your-fo-environment.dynamics.com",
|
278
|
+
credential_source="keyvault", # Use Azure Key Vault for credentials
|
279
|
+
keyvault_url="https://your-keyvault.vault.azure.net/"
|
280
|
+
)
|
281
|
+
|
282
|
+
# Option 4: With custom settings
|
266
283
|
config = FOClientConfig(
|
267
284
|
base_url="https://your-fo-environment.dynamics.com",
|
268
285
|
use_default_credentials=True,
|
@@ -462,7 +479,9 @@ d365fo-client/
|
|
462
479
|
│ ├── metadata.py # Legacy metadata operations
|
463
480
|
│ ├── metadata_api.py # Metadata API client
|
464
481
|
│ ├── metadata_cache.py # Metadata caching layer V2
|
465
|
-
│ ├── metadata_sync.py # Metadata synchronization V2
|
482
|
+
│ ├── metadata_sync.py # Metadata synchronization V2 with session management
|
483
|
+
│ ├── sync_session.py # Enhanced sync session management (New in v0.2.3)
|
484
|
+
│ ├── credential_manager.py # Credential source management (New in v0.2.3)
|
466
485
|
│ ├── labels.py # Label operations V2
|
467
486
|
│ ├── profiles.py # Profile data models
|
468
487
|
│ ├── profile_manager.py # Profile management
|
@@ -519,6 +538,8 @@ d365fo-client/
|
|
519
538
|
| `client_secret` | str | None | Azure AD client secret |
|
520
539
|
| `tenant_id` | str | None | Azure AD tenant ID |
|
521
540
|
| `use_default_credentials` | bool | True | Use Azure Default Credential |
|
541
|
+
| `credential_source` | str | "environment" | Credential source: "environment", "keyvault" |
|
542
|
+
| `keyvault_url` | str | None | Azure Key Vault URL for credential storage |
|
522
543
|
| `verify_ssl` | bool | False | Verify SSL certificates |
|
523
544
|
| `timeout` | int | 30 | Request timeout in seconds |
|
524
545
|
| `metadata_cache_dir` | str | Platform-specific user cache | Metadata cache directory |
|
@@ -619,9 +640,9 @@ cp tests/integration/.env.template tests/integration/.env
|
|
619
640
|
# Edit .env file with your settings:
|
620
641
|
INTEGRATION_TEST_LEVEL=sandbox
|
621
642
|
D365FO_SANDBOX_BASE_URL=https://your-test.dynamics.com
|
622
|
-
|
623
|
-
|
624
|
-
|
643
|
+
D365FO_CLIENT_ID=your-client-id
|
644
|
+
D365FO_CLIENT_SECRET=your-client-secret
|
645
|
+
D365FO_TENANT_ID=your-tenant-id
|
625
646
|
```
|
626
647
|
|
627
648
|
#### Available Commands
|
@@ -707,9 +728,9 @@ pip install d365fo-client
|
|
707
728
|
|
708
729
|
# Set up environment variables
|
709
730
|
export D365FO_BASE_URL="https://your-environment.dynamics.com"
|
710
|
-
export
|
711
|
-
export
|
712
|
-
export
|
731
|
+
export D365FO_CLIENT_ID="your-client-id" # Optional with default credentials
|
732
|
+
export D365FO_CLIENT_SECRET="your-client-secret" # Optional with default credentials
|
733
|
+
export D365FO_TENANT_ID="your-tenant-id" # Optional with default credentials
|
713
734
|
|
714
735
|
# Start the MCP server
|
715
736
|
d365fo-mcp-server
|
@@ -857,9 +878,19 @@ For service principal authentication:
|
|
857
878
|
|
858
879
|
```bash
|
859
880
|
export D365FO_BASE_URL="https://your-environment.dynamics.com"
|
860
|
-
export
|
861
|
-
export
|
862
|
-
export
|
881
|
+
export D365FO_CLIENT_ID="your-client-id"
|
882
|
+
export D365FO_CLIENT_SECRET="your-client-secret"
|
883
|
+
export D365FO_TENANT_ID="your-tenant-id"
|
884
|
+
d365fo-mcp-server
|
885
|
+
```
|
886
|
+
|
887
|
+
#### Azure Key Vault Integration (New in v0.2.3)
|
888
|
+
For secure credential storage using Azure Key Vault:
|
889
|
+
|
890
|
+
```bash
|
891
|
+
export D365FO_BASE_URL="https://your-environment.dynamics.com"
|
892
|
+
export D365FO_CREDENTIAL_SOURCE="keyvault"
|
893
|
+
export D365FO_KEYVAULT_URL="https://your-keyvault.vault.azure.net/"
|
863
894
|
d365fo-mcp-server
|
864
895
|
```
|
865
896
|
|
@@ -1032,7 +1063,7 @@ tail -f ~/.d365fo-mcp/logs/mcp-server.log
|
|
1032
1063
|
az account show
|
1033
1064
|
|
1034
1065
|
# Test with explicit credentials
|
1035
|
-
export
|
1066
|
+
export D365FO_CLIENT_ID="your-client-id"
|
1036
1067
|
# ... set other variables
|
1037
1068
|
d365fo-mcp-server
|
1038
1069
|
```
|
@@ -1,25 +1,27 @@
|
|
1
1
|
d365fo_client/__init__.py,sha256=EH0lGM1TDt_-hVJqnuZK4i7sKSttSEhHapl0tRdRrmE,7752
|
2
|
-
d365fo_client/auth.py,sha256=
|
2
|
+
d365fo_client/auth.py,sha256=XKt6MJQrnR1v5ahA1OR3Q-1LpHY3xo-mA62a4kvmO1M,4633
|
3
3
|
d365fo_client/cli.py,sha256=fBF6OjbZgj0T--Sii1wazP2fIFZnZnEeznUSti3l194,26118
|
4
|
-
d365fo_client/client.py,sha256=
|
4
|
+
d365fo_client/client.py,sha256=00d1V_kz6-upPMvKMSs42jXej2N7lAznna3KxQJYyCo,55326
|
5
5
|
d365fo_client/config.py,sha256=WQxXEHYhgXaM2Ocd98ILiAnJycevKArn1FNp-TGKdKA,10469
|
6
|
+
d365fo_client/credential_sources.py,sha256=XJ-DitJD3f0ncTMP4Cj0-O1RW4dFGiicYJojzg4HL_w,17259
|
6
7
|
d365fo_client/crud.py,sha256=YBjYIKqgyWYsLB8uRtI_sjRa2G7d9qtgm5mLGCB2CfA,6701
|
7
8
|
d365fo_client/exceptions.py,sha256=k8tVb5K6C_F4sCY5NVoE5RYpKPjCgPLd3JtcLIuXzTw,733
|
8
9
|
d365fo_client/labels.py,sha256=a7b_YYWDWaXANirDAr-CoodEpBvfaZvRGy76kNg4Z44,18412
|
9
10
|
d365fo_client/main.py,sha256=6jBBcePWHOG-FMJ8HMz9Qz1Tmw486Ef6VGbfvK5oBHg,19045
|
10
|
-
d365fo_client/metadata_api.py,sha256=
|
11
|
-
d365fo_client/models.py,sha256=
|
11
|
+
d365fo_client/metadata_api.py,sha256=1tDkPLxvR3F4St8GjHssn9EAoNa-ZDEP0-pbGa84DLI,41624
|
12
|
+
d365fo_client/models.py,sha256=MZoDAiXC0itaGjhgzxAzYR7JxgYwkR3035u3VHgYiNo,27309
|
12
13
|
d365fo_client/output.py,sha256=U-q6_wRHARWUKEhK-OCu16VhgWZ89sofzZuE67hNg1Q,5986
|
13
|
-
d365fo_client/profile_manager.py,sha256=
|
14
|
-
d365fo_client/profiles.py,sha256=
|
14
|
+
d365fo_client/profile_manager.py,sha256=w5yV1Ab2hZDpYQjssr2ztcOswXbAYCfy5Bl56YhuVwg,11609
|
15
|
+
d365fo_client/profiles.py,sha256=M_PlJ42ahNCkQr5W5eXdile_ljGOnqueHOQJboVwHlo,7147
|
15
16
|
d365fo_client/query.py,sha256=wOZjXEtGzPcd5mRfdkpMZTHZdSId44gLSboJs4LeSaw,5028
|
16
17
|
d365fo_client/session.py,sha256=4OIR61eYQHALPmbXZ446Ko4j5ttvozDxDRnMikogBys,1841
|
18
|
+
d365fo_client/sync_models.py,sha256=svQGwf9PDOBot-8bo6jiUHL4L62qtICHrhuM5RXGbOs,6419
|
17
19
|
d365fo_client/utils.py,sha256=9bHSWznuhuOmxbx9Vkd8k9RFctHYUCTiWZCmcUujPqw,6771
|
18
20
|
d365fo_client/mcp/__init__.py,sha256=B6Pw342ejRUKrw0NN5zyMSb1lF2rTICxv8KFfBMlBsU,487
|
19
|
-
d365fo_client/mcp/client_manager.py,sha256=
|
20
|
-
d365fo_client/mcp/main.py,sha256=
|
21
|
+
d365fo_client/mcp/client_manager.py,sha256=Z8jNm8pjI64-xloJW5mcLe-7tP1JD6iGxwsaynLgyNs,12169
|
22
|
+
d365fo_client/mcp/main.py,sha256=mlKnxHHo79NBYAwcNHXuB5ImuE6QDcJvCVBtktbdduM,4269
|
21
23
|
d365fo_client/mcp/models.py,sha256=Tq48Xvh6aXkGAHZ805r1OwPIzftUGXSYE-rt0pY4wI8,7493
|
22
|
-
d365fo_client/mcp/server.py,sha256=
|
24
|
+
d365fo_client/mcp/server.py,sha256=n4BlgfJkMuRZbHD8jshZOS-oPH-Pi2uKBExE4oxeo3U,24964
|
23
25
|
d365fo_client/mcp/prompts/__init__.py,sha256=haa0Cit3f2xWrcqlFkhfQTqff2DfAore_I0il2VIW_0,1104
|
24
26
|
d365fo_client/mcp/prompts/action_execution.py,sha256=gdZcgtXHrn8fUBfk2758RGbxwAcu1qRODOxVqwLoeZA,15587
|
25
27
|
d365fo_client/mcp/prompts/sequence_analysis.py,sha256=AMsb3boD0rdd4xUYrnEqvx_OTYcFIyo6acE9zMJzgOU,12662
|
@@ -29,24 +31,26 @@ d365fo_client/mcp/resources/entity_handler.py,sha256=wFYMPp_Rny-HZams5NtIFF8vrYU
|
|
29
31
|
d365fo_client/mcp/resources/environment_handler.py,sha256=iblut_E_0q9Z4gfOeEG5bfodTPOzFcvfiYQFzH3WggE,4469
|
30
32
|
d365fo_client/mcp/resources/metadata_handler.py,sha256=ZPH4yAylASpbAPptWMHazACKS3dhDQKr88o22KBqRsM,11618
|
31
33
|
d365fo_client/mcp/resources/query_handler.py,sha256=NMJ07SolMO_rjdY9l7m53_EV2j0KHQSrckTySnTw1oU,4635
|
32
|
-
d365fo_client/mcp/tools/__init__.py,sha256=
|
34
|
+
d365fo_client/mcp/tools/__init__.py,sha256=kfPPcs2r6biL_KLn-1MqSN2tU2eD5QahocMIun7yfuU,463
|
33
35
|
d365fo_client/mcp/tools/connection_tools.py,sha256=TT4eDF0Bb8gS36nTj5K-AIuuAYCZMB-UjW66w5HLi3I,6872
|
34
36
|
d365fo_client/mcp/tools/crud_tools.py,sha256=YM6TDK41f7-xg9K4d4vs3t5vTwabtru9Z7gnzup3XvE,31229
|
35
37
|
d365fo_client/mcp/tools/database_tools.py,sha256=Kh9g-e0RCTKPbmsuZ99MDK8-ZYaBB2czbElHDaJXPGU,33286
|
36
38
|
d365fo_client/mcp/tools/label_tools.py,sha256=BpUI55MgDjhokN44JhZLh1J46JgiPL0Fh0pfhZrD5I0,6376
|
37
39
|
d365fo_client/mcp/tools/metadata_tools.py,sha256=NHFrQbpL0403EA2mxLqqRE0q7MDPLroZ79JRb3gqxeQ,32260
|
38
|
-
d365fo_client/mcp/tools/profile_tools.py,sha256=
|
40
|
+
d365fo_client/mcp/tools/profile_tools.py,sha256=n8-uGbA-K4kFVBZ--yfk-9znyPydiwCoWNEm9EpULwg,42337
|
41
|
+
d365fo_client/mcp/tools/sync_tools.py,sha256=nHpwDwGttYTVenrhl4PygWRguZj37IfHeZeqRCqpg6w,22358
|
39
42
|
d365fo_client/metadata_v2/__init__.py,sha256=54VTuWSo5j-BEM-raLbRr3bfxc5lVXp2COajsy-7Oo0,1895
|
40
|
-
d365fo_client/metadata_v2/cache_v2.py,sha256=
|
43
|
+
d365fo_client/metadata_v2/cache_v2.py,sha256=lW9QiPXzv__rjhswNiiRgsoOyistk5tBYhX-WjmhQqE,60476
|
41
44
|
d365fo_client/metadata_v2/database_v2.py,sha256=JX6kB_xGc1QrHSgo0XBdeBWj_k3n_ZwYtx2fsxV5ufI,25584
|
42
|
-
d365fo_client/metadata_v2/global_version_manager.py,sha256=
|
45
|
+
d365fo_client/metadata_v2/global_version_manager.py,sha256=dbf7ISFGCU88mDK5gtEZa9-q0DLLbWkLiiJIVJ_9W60,22537
|
43
46
|
d365fo_client/metadata_v2/label_utils.py,sha256=-GRagiURJv-ZQCepeOkCRqfo62rhWo8A42271w0Cb6A,3752
|
44
47
|
d365fo_client/metadata_v2/search_engine_v2.py,sha256=s_XVqP3LLog19IAv8DpxVUS7TFRUAjuBevyfe1Ldwps,16525
|
45
|
-
d365fo_client/metadata_v2/sync_manager_v2.py,sha256=
|
48
|
+
d365fo_client/metadata_v2/sync_manager_v2.py,sha256=iNwnKxHdE-kIbPUvBmRYJjrHEbyLMNt___ISXEnz3FM,34060
|
49
|
+
d365fo_client/metadata_v2/sync_session_manager.py,sha256=4v3idTQF3Mhaky7DD3R4AY74ZAt1s5mIuHlekIVH9MY,44350
|
46
50
|
d365fo_client/metadata_v2/version_detector.py,sha256=t9mKaeT4SKb13LmIq5fB6PTLOZn5Jp7ZUqQobntNEUg,15791
|
47
|
-
d365fo_client-0.2.
|
48
|
-
d365fo_client-0.2.
|
49
|
-
d365fo_client-0.2.
|
50
|
-
d365fo_client-0.2.
|
51
|
-
d365fo_client-0.2.
|
52
|
-
d365fo_client-0.2.
|
51
|
+
d365fo_client-0.2.3.dist-info/licenses/LICENSE,sha256=idD7NJAZD7ognzZVyKjDxVYDCmngEIt0WxA_uB1v0iI,1071
|
52
|
+
d365fo_client-0.2.3.dist-info/METADATA,sha256=0D0rsWk6VAfStAuuoujNQwoDE_0EC9dR3tXHyoHgeKU,36940
|
53
|
+
d365fo_client-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
54
|
+
d365fo_client-0.2.3.dist-info/entry_points.txt,sha256=ZZBjH4mQ0XO3ALeNswswa09dh_JGIvD-zCBkKi6qNqA,106
|
55
|
+
d365fo_client-0.2.3.dist-info/top_level.txt,sha256=ZbvqO90RjhOW0cjFCAEeP8OFyITbhrij2vC3k4bWERQ,14
|
56
|
+
d365fo_client-0.2.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|