d365fo-client 0.2.4__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- d365fo_client/__init__.py +7 -1
- d365fo_client/auth.py +9 -21
- d365fo_client/cli.py +25 -13
- d365fo_client/client.py +8 -4
- d365fo_client/config.py +52 -30
- d365fo_client/credential_sources.py +5 -0
- d365fo_client/main.py +1 -1
- d365fo_client/mcp/__init__.py +3 -1
- d365fo_client/mcp/auth_server/__init__.py +5 -0
- d365fo_client/mcp/auth_server/auth/__init__.py +30 -0
- d365fo_client/mcp/auth_server/auth/auth.py +372 -0
- d365fo_client/mcp/auth_server/auth/oauth_proxy.py +989 -0
- d365fo_client/mcp/auth_server/auth/providers/__init__.py +0 -0
- d365fo_client/mcp/auth_server/auth/providers/azure.py +325 -0
- d365fo_client/mcp/auth_server/auth/providers/bearer.py +25 -0
- d365fo_client/mcp/auth_server/auth/providers/jwt.py +547 -0
- d365fo_client/mcp/auth_server/auth/redirect_validation.py +65 -0
- d365fo_client/mcp/auth_server/dependencies.py +136 -0
- d365fo_client/mcp/client_manager.py +16 -67
- d365fo_client/mcp/fastmcp_main.py +358 -0
- d365fo_client/mcp/fastmcp_server.py +598 -0
- d365fo_client/mcp/fastmcp_utils.py +431 -0
- d365fo_client/mcp/main.py +40 -13
- d365fo_client/mcp/mixins/__init__.py +24 -0
- d365fo_client/mcp/mixins/base_tools_mixin.py +55 -0
- d365fo_client/mcp/mixins/connection_tools_mixin.py +50 -0
- d365fo_client/mcp/mixins/crud_tools_mixin.py +311 -0
- d365fo_client/mcp/mixins/database_tools_mixin.py +685 -0
- d365fo_client/mcp/mixins/label_tools_mixin.py +87 -0
- d365fo_client/mcp/mixins/metadata_tools_mixin.py +565 -0
- d365fo_client/mcp/mixins/performance_tools_mixin.py +109 -0
- d365fo_client/mcp/mixins/profile_tools_mixin.py +713 -0
- d365fo_client/mcp/mixins/sync_tools_mixin.py +321 -0
- d365fo_client/mcp/prompts/action_execution.py +1 -1
- d365fo_client/mcp/prompts/sequence_analysis.py +1 -1
- d365fo_client/mcp/tools/crud_tools.py +3 -3
- d365fo_client/mcp/tools/sync_tools.py +1 -1
- d365fo_client/mcp/utilities/__init__.py +1 -0
- d365fo_client/mcp/utilities/auth.py +34 -0
- d365fo_client/mcp/utilities/logging.py +58 -0
- d365fo_client/mcp/utilities/types.py +426 -0
- d365fo_client/metadata_v2/sync_manager_v2.py +2 -0
- d365fo_client/metadata_v2/sync_session_manager.py +7 -7
- d365fo_client/models.py +139 -139
- d365fo_client/output.py +2 -2
- d365fo_client/profile_manager.py +62 -27
- d365fo_client/profiles.py +118 -113
- d365fo_client/settings.py +355 -0
- d365fo_client/sync_models.py +85 -2
- d365fo_client/utils.py +2 -1
- {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.0.dist-info}/METADATA +273 -18
- d365fo_client-0.3.0.dist-info/RECORD +84 -0
- d365fo_client-0.3.0.dist-info/entry_points.txt +4 -0
- d365fo_client-0.2.4.dist-info/RECORD +0 -56
- d365fo_client-0.2.4.dist-info/entry_points.txt +0 -3
- {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.0.dist-info}/WHEEL +0 -0
- {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {d365fo_client-0.2.4.dist-info → d365fo_client-0.3.0.dist-info}/top_level.txt +0 -0
d365fo_client/models.py
CHANGED
@@ -8,7 +8,7 @@ from enum import Enum, StrEnum
|
|
8
8
|
from pathlib import Path
|
9
9
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
10
10
|
|
11
|
-
from .utils import
|
11
|
+
from .utils import get_default_cache_directory
|
12
12
|
|
13
13
|
if TYPE_CHECKING:
|
14
14
|
from typing import ForwardRef
|
@@ -60,18 +60,6 @@ class ODataBindingKind(StrEnum):
|
|
60
60
|
UNBOUND = "Unbound"
|
61
61
|
|
62
62
|
|
63
|
-
class SyncStrategy(StrEnum):
|
64
|
-
"""Metadata synchronization strategies"""
|
65
|
-
|
66
|
-
FULL = "full"
|
67
|
-
INCREMENTAL = "incremental"
|
68
|
-
ENTITIES_ONLY = "entities_only"
|
69
|
-
LABELS_ONLY = "labels_only"
|
70
|
-
SHARING_MODE = "sharing_mode"
|
71
|
-
FULL_WITHOUT_LABELS = "full_without_labels"
|
72
|
-
|
73
|
-
|
74
|
-
|
75
63
|
class Cardinality(StrEnum):
|
76
64
|
"""Navigation Property Cardinality"""
|
77
65
|
|
@@ -81,39 +69,142 @@ class Cardinality(StrEnum):
|
|
81
69
|
|
82
70
|
@dataclass
|
83
71
|
class FOClientConfig:
|
84
|
-
"""Configuration for F&O Client
|
72
|
+
"""Configuration for F&O Client
|
73
|
+
|
74
|
+
This class handles all configuration options for connecting to and interacting
|
75
|
+
with Microsoft Dynamics 365 Finance & Operations environments.
|
85
76
|
|
77
|
+
Authentication is handled through credential_source:
|
78
|
+
- If None: Uses Azure Default Credentials (DefaultAzureCredential)
|
79
|
+
- If provided: Uses the specified credential source (environment vars, Key Vault, etc.)
|
80
|
+
"""
|
81
|
+
|
82
|
+
# Core connection settings
|
86
83
|
base_url: str
|
87
|
-
auth_mode: Optional[str] = "defaut" # default | client_credentials
|
88
|
-
client_id: Optional[str] = None
|
89
|
-
client_secret: Optional[str] = None
|
90
|
-
tenant_id: Optional[str] = None
|
91
|
-
use_default_credentials: bool = True
|
92
84
|
verify_ssl: bool = False
|
93
|
-
metadata_cache_dir: str = None
|
94
85
|
timeout: int = 30
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
86
|
+
|
87
|
+
# Authentication - unified through credential source
|
88
|
+
credential_source: Optional["CredentialSource"] = None
|
89
|
+
|
90
|
+
# Cache configuration
|
91
|
+
metadata_cache_dir: Optional[str] = None
|
99
92
|
enable_metadata_cache: bool = True
|
100
|
-
|
93
|
+
use_cache_first: bool = True
|
101
94
|
cache_ttl_seconds: int = 300
|
102
|
-
enable_fts_search: bool = True
|
103
95
|
max_memory_cache_size: int = 1000
|
104
|
-
|
105
|
-
|
106
|
-
#
|
107
|
-
|
96
|
+
enable_fts_search: bool = True
|
97
|
+
|
98
|
+
# Label cache settings
|
99
|
+
use_label_cache: bool = True
|
100
|
+
label_cache_expiry_minutes: int = 60
|
101
|
+
|
102
|
+
# Sync configuration
|
103
|
+
metadata_sync_interval_minutes: int = 60
|
104
|
+
language: str = "en-US"
|
108
105
|
|
109
106
|
def __post_init__(self):
|
110
|
-
"""Post-initialization
|
107
|
+
"""Post-initialization validation and setup."""
|
108
|
+
# Set default cache directory if not provided
|
111
109
|
if self.metadata_cache_dir is None:
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
110
|
+
self.metadata_cache_dir = get_default_cache_directory()
|
111
|
+
|
112
|
+
# Validate configuration
|
113
|
+
self._validate_config()
|
114
|
+
|
115
|
+
def _validate_config(self) -> None:
|
116
|
+
"""Validate configuration parameters."""
|
117
|
+
if not self.base_url:
|
118
|
+
raise ValueError("base_url is required")
|
119
|
+
|
120
|
+
if not self.base_url.startswith(("http://", "https://")):
|
121
|
+
raise ValueError("base_url must start with http:// or https://")
|
122
|
+
|
123
|
+
if self.timeout <= 0:
|
124
|
+
raise ValueError("timeout must be greater than 0")
|
125
|
+
|
126
|
+
if self.label_cache_expiry_minutes <= 0:
|
127
|
+
raise ValueError("label_cache_expiry_minutes must be greater than 0")
|
128
|
+
|
129
|
+
if self.metadata_sync_interval_minutes <= 0:
|
130
|
+
raise ValueError("metadata_sync_interval_minutes must be greater than 0")
|
131
|
+
|
132
|
+
if self.cache_ttl_seconds <= 0:
|
133
|
+
raise ValueError("cache_ttl_seconds must be greater than 0")
|
134
|
+
|
135
|
+
if self.max_memory_cache_size <= 0:
|
136
|
+
raise ValueError("max_memory_cache_size must be greater than 0")
|
137
|
+
|
138
|
+
@property
|
139
|
+
def uses_default_credentials(self) -> bool:
|
140
|
+
"""Check if using Azure Default Credentials."""
|
141
|
+
return self.credential_source is None
|
142
|
+
|
143
|
+
@property
|
144
|
+
def uses_credential_source(self) -> bool:
|
145
|
+
"""Check if using a specific credential source."""
|
146
|
+
return self.credential_source is not None
|
147
|
+
|
148
|
+
def to_dict(self) -> Dict[str, Any]:
|
149
|
+
"""Convert to dictionary for serialization."""
|
150
|
+
from dataclasses import asdict
|
151
|
+
data = asdict(self)
|
152
|
+
|
153
|
+
# Handle credential_source serialization
|
154
|
+
if self.credential_source is not None:
|
155
|
+
data["credential_source"] = self.credential_source.to_dict()
|
156
|
+
|
157
|
+
return data
|
158
|
+
|
159
|
+
@classmethod
|
160
|
+
def from_dict(cls, data: Dict[str, Any]) -> "FOClientConfig":
|
161
|
+
"""Create from dictionary with validation and migration."""
|
162
|
+
# Migrate legacy credential fields to credential_source
|
163
|
+
migrated_data = cls._migrate_legacy_credentials(data.copy())
|
164
|
+
|
165
|
+
# Handle credential_source deserialization
|
166
|
+
if "credential_source" in migrated_data and migrated_data["credential_source"] is not None:
|
167
|
+
from .credential_sources import CredentialSource
|
168
|
+
credential_source_data = migrated_data["credential_source"]
|
169
|
+
try:
|
170
|
+
migrated_data["credential_source"] = CredentialSource.from_dict(credential_source_data)
|
171
|
+
except Exception:
|
172
|
+
migrated_data["credential_source"] = None
|
173
|
+
|
174
|
+
# Filter out unknown and deprecated fields
|
175
|
+
valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
|
176
|
+
filtered_data = {k: v for k, v in migrated_data.items() if k in valid_fields}
|
177
|
+
|
178
|
+
return cls(**filtered_data)
|
179
|
+
|
180
|
+
@classmethod
|
181
|
+
def _migrate_legacy_credentials(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
182
|
+
"""Migrate legacy credential fields to credential_source."""
|
183
|
+
# Check for legacy credential fields
|
184
|
+
legacy_fields = ["client_id", "client_secret", "tenant_id", "auth_mode", "use_default_credentials"]
|
185
|
+
has_legacy_creds = any(field in data for field in legacy_fields)
|
186
|
+
|
187
|
+
if has_legacy_creds and "credential_source" not in data:
|
188
|
+
# Determine if we should use default credentials
|
189
|
+
use_default = data.get("use_default_credentials", True)
|
190
|
+
# Check if explicit credentials are provided
|
191
|
+
client_id = data.get("client_id")
|
192
|
+
client_secret = data.get("client_secret")
|
193
|
+
tenant_id = data.get("tenant_id")
|
194
|
+
|
195
|
+
has_explicit_creds = all([client_id, client_secret, tenant_id])
|
196
|
+
|
197
|
+
if not use_default and has_explicit_creds:
|
198
|
+
# Create environment credential source for backward compatibility
|
199
|
+
from .credential_sources import EnvironmentCredentialSource
|
200
|
+
data["credential_source"] = EnvironmentCredentialSource().to_dict()
|
201
|
+
# If use_default or no explicit creds, credential_source remains None (default creds)
|
202
|
+
|
203
|
+
# Remove legacy fields
|
204
|
+
for field in legacy_fields:
|
205
|
+
data.pop(field, None)
|
206
|
+
|
207
|
+
return data
|
117
208
|
|
118
209
|
|
119
210
|
@dataclass
|
@@ -486,20 +577,20 @@ class ActionTypeInfo:
|
|
486
577
|
}
|
487
578
|
|
488
579
|
|
489
|
-
@dataclass
|
490
|
-
class ActionParameterInfo:
|
491
|
-
|
580
|
+
# @dataclass
|
581
|
+
# class ActionParameterInfo:
|
582
|
+
# """Enhanced action parameter information"""
|
492
583
|
|
493
|
-
|
494
|
-
|
495
|
-
|
584
|
+
# name: str
|
585
|
+
# type: "ActionTypeInfo"
|
586
|
+
# parameter_order: int = 0
|
496
587
|
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
588
|
+
# def to_dict(self) -> Dict[str, Any]:
|
589
|
+
# return {
|
590
|
+
# "name": self.name,
|
591
|
+
# "type": self.type.to_dict(),
|
592
|
+
# "parameter_order": self.parameter_order,
|
593
|
+
# }
|
503
594
|
|
504
595
|
|
505
596
|
@dataclass
|
@@ -602,21 +693,6 @@ class SearchResults:
|
|
602
693
|
}
|
603
694
|
|
604
695
|
|
605
|
-
@dataclass
|
606
|
-
class SyncResult:
|
607
|
-
"""Metadata synchronization result"""
|
608
|
-
|
609
|
-
sync_type: str # full|incremental|skipped
|
610
|
-
entities_synced: int = 0
|
611
|
-
actions_synced: int = 0
|
612
|
-
enumerations_synced: int = 0
|
613
|
-
labels_synced: int = 0
|
614
|
-
duration_ms: float = 0.0
|
615
|
-
success: bool = True
|
616
|
-
errors: List[str] = field(default_factory=list)
|
617
|
-
reason: Optional[str] = None
|
618
|
-
|
619
|
-
|
620
696
|
# ============================================================================
|
621
697
|
# Enhanced V2 Models for Advanced Metadata Caching
|
622
698
|
# ============================================================================
|
@@ -812,79 +888,3 @@ class VersionDetectionResult:
|
|
812
888
|
}
|
813
889
|
|
814
890
|
|
815
|
-
@dataclass
|
816
|
-
class SyncResultV2:
|
817
|
-
"""Enhanced synchronization result for v2 with sharing metrics"""
|
818
|
-
|
819
|
-
sync_type: str # full|incremental|linked|skipped|failed
|
820
|
-
entities_synced: int = 0
|
821
|
-
actions_synced: int = 0
|
822
|
-
enumerations_synced: int = 0
|
823
|
-
labels_synced: int = 0
|
824
|
-
duration_ms: float = 0.0
|
825
|
-
success: bool = True
|
826
|
-
errors: List[str] = field(default_factory=list)
|
827
|
-
reason: Optional[str] = None
|
828
|
-
# V2 specific fields
|
829
|
-
global_version_id: Optional[int] = None
|
830
|
-
was_shared: bool = False
|
831
|
-
reference_count: int = 1
|
832
|
-
storage_saved_bytes: int = 0
|
833
|
-
|
834
|
-
def to_dict(self) -> Dict[str, Any]:
|
835
|
-
"""Convert to dictionary for JSON serialization"""
|
836
|
-
return {
|
837
|
-
"sync_type": self.sync_type,
|
838
|
-
"entities_synced": self.entities_synced,
|
839
|
-
"actions_synced": self.actions_synced,
|
840
|
-
"enumerations_synced": self.enumerations_synced,
|
841
|
-
"labels_synced": self.labels_synced,
|
842
|
-
"duration_ms": self.duration_ms,
|
843
|
-
"success": self.success,
|
844
|
-
"errors": self.errors,
|
845
|
-
"reason": self.reason,
|
846
|
-
"global_version_id": self.global_version_id,
|
847
|
-
"was_shared": self.was_shared,
|
848
|
-
"reference_count": self.reference_count,
|
849
|
-
"storage_saved_bytes": self.storage_saved_bytes,
|
850
|
-
}
|
851
|
-
|
852
|
-
|
853
|
-
@dataclass
|
854
|
-
class SyncProgress:
|
855
|
-
"""Sync progress tracking"""
|
856
|
-
|
857
|
-
global_version_id: int
|
858
|
-
strategy: SyncStrategy
|
859
|
-
phase: str
|
860
|
-
total_steps: int
|
861
|
-
completed_steps: int
|
862
|
-
current_operation: str
|
863
|
-
start_time: datetime
|
864
|
-
estimated_completion: Optional[datetime] = None
|
865
|
-
error: Optional[str] = None
|
866
|
-
|
867
|
-
|
868
|
-
@dataclass
|
869
|
-
class SyncResult:
|
870
|
-
"""Sync operation result"""
|
871
|
-
|
872
|
-
success: bool
|
873
|
-
error: Optional[str]
|
874
|
-
duration_ms: int
|
875
|
-
entity_count: int
|
876
|
-
action_count: int
|
877
|
-
enumeration_count: int
|
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/output.py
CHANGED
@@ -163,12 +163,12 @@ class OutputFormatter:
|
|
163
163
|
|
164
164
|
def format_success_message(message: str) -> str:
|
165
165
|
"""Format a success message with checkmark."""
|
166
|
-
return f"
|
166
|
+
return f"✅ {message}"
|
167
167
|
|
168
168
|
|
169
169
|
def format_error_message(message: str) -> str:
|
170
170
|
"""Format an error message with X mark."""
|
171
|
-
return f"
|
171
|
+
return f"❌ {message}"
|
172
172
|
|
173
173
|
|
174
174
|
def format_info_message(message: str) -> str:
|
d365fo_client/profile_manager.py
CHANGED
@@ -66,10 +66,6 @@ class ProfileManager:
|
|
66
66
|
self,
|
67
67
|
name: str,
|
68
68
|
base_url: str,
|
69
|
-
auth_mode: str = "default",
|
70
|
-
client_id: Optional[str] = None,
|
71
|
-
client_secret: Optional[str] = None,
|
72
|
-
tenant_id: Optional[str] = None,
|
73
69
|
verify_ssl: bool = True,
|
74
70
|
timeout: int = 60,
|
75
71
|
use_label_cache: bool = True,
|
@@ -78,26 +74,31 @@ class ProfileManager:
|
|
78
74
|
language: str = "en-US",
|
79
75
|
cache_dir: Optional[str] = None,
|
80
76
|
description: Optional[str] = None,
|
81
|
-
credential_source: Optional["CredentialSource"] = None
|
82
|
-
|
77
|
+
credential_source: Optional["CredentialSource"] = None,
|
78
|
+
# Legacy parameters for backward compatibility - will be converted to credential_source
|
79
|
+
auth_mode: Optional[str] = None,
|
80
|
+
client_id: Optional[str] = None,
|
81
|
+
client_secret: Optional[str] = None,
|
82
|
+
tenant_id: Optional[str] = None,
|
83
83
|
) -> bool:
|
84
84
|
"""Create a new profile.
|
85
85
|
|
86
86
|
Args:
|
87
87
|
name: Profile name
|
88
88
|
base_url: D365FO base URL
|
89
|
-
auth_mode: Authentication mode
|
90
|
-
client_id: Azure client ID (optional)
|
91
|
-
client_secret: Azure client secret (optional)
|
92
|
-
tenant_id: Azure tenant ID (optional)
|
93
89
|
verify_ssl: Whether to verify SSL certificates
|
94
90
|
timeout: Request timeout in seconds
|
95
91
|
use_label_cache: Whether to enable label caching
|
96
92
|
label_cache_expiry_minutes: Label cache expiry in minutes
|
93
|
+
use_cache_first: Whether to use cache-first behavior
|
97
94
|
language: Default language code
|
98
95
|
cache_dir: Cache directory path
|
99
|
-
description: Profile description
|
100
|
-
credential_source: Credential source configuration
|
96
|
+
description: Profile description
|
97
|
+
credential_source: Credential source configuration (preferred method)
|
98
|
+
auth_mode: [DEPRECATED] Authentication mode - will be converted to credential_source
|
99
|
+
client_id: [DEPRECATED] Azure client ID - will be converted to credential_source
|
100
|
+
client_secret: [DEPRECATED] Azure client secret - will be converted to credential_source
|
101
|
+
tenant_id: [DEPRECATED] Azure tenant ID - will be converted to credential_source
|
101
102
|
|
102
103
|
Returns:
|
103
104
|
True if created successfully
|
@@ -108,24 +109,27 @@ class ProfileManager:
|
|
108
109
|
logger.error(f"Profile already exists: {name}")
|
109
110
|
return False
|
110
111
|
|
112
|
+
# Handle legacy credential parameters by creating credential_source
|
113
|
+
effective_credential_source = credential_source
|
114
|
+
if effective_credential_source is None and any([auth_mode, client_id, client_secret, tenant_id]):
|
115
|
+
effective_credential_source = self._create_credential_source_from_legacy(
|
116
|
+
auth_mode, client_id, client_secret, tenant_id
|
117
|
+
)
|
118
|
+
|
111
119
|
# Create unified profile
|
112
120
|
profile = Profile(
|
113
121
|
name=name,
|
114
122
|
base_url=base_url,
|
115
|
-
auth_mode=auth_mode,
|
116
|
-
client_id=client_id,
|
117
|
-
client_secret=client_secret,
|
118
|
-
tenant_id=tenant_id,
|
119
123
|
verify_ssl=verify_ssl,
|
120
124
|
timeout=timeout,
|
121
125
|
use_label_cache=use_label_cache,
|
122
126
|
label_cache_expiry_minutes=label_cache_expiry_minutes,
|
123
127
|
use_cache_first=use_cache_first,
|
124
128
|
language=language,
|
125
|
-
|
129
|
+
metadata_cache_dir=cache_dir, # Map cache_dir parameter to metadata_cache_dir field
|
126
130
|
description=description,
|
127
131
|
output_format="table", # Default for CLI compatibility
|
128
|
-
credential_source=
|
132
|
+
credential_source=effective_credential_source
|
129
133
|
)
|
130
134
|
|
131
135
|
self.config_manager.save_profile(profile)
|
@@ -293,7 +297,7 @@ class ProfileManager:
|
|
293
297
|
export_data = {"version": "1.0", "profiles": {}}
|
294
298
|
|
295
299
|
for name, profile in profiles.items():
|
296
|
-
export_data["profiles"][name] =
|
300
|
+
export_data["profiles"][name] = profile.to_dict()
|
297
301
|
|
298
302
|
with open(file_path, "w", encoding="utf-8") as f:
|
299
303
|
yaml.dump(export_data, f, default_flow_style=False, allow_unicode=True)
|
@@ -339,14 +343,13 @@ class ProfileManager:
|
|
339
343
|
if overwrite and self.get_profile(name):
|
340
344
|
self.delete_profile(name)
|
341
345
|
|
342
|
-
#
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
results[name] = success
|
346
|
+
# Create profile directly using Profile.create_from_dict
|
347
|
+
# This handles all fields including credential_source properly
|
348
|
+
profile = Profile.create_from_dict(name, profile_data)
|
349
|
+
|
350
|
+
# Save the profile directly via config manager
|
351
|
+
self.config_manager.save_profile(profile)
|
352
|
+
results[name] = True
|
350
353
|
|
351
354
|
except Exception as e:
|
352
355
|
logger.error(f"Error importing profile {name}: {e}")
|
@@ -358,3 +361,35 @@ class ProfileManager:
|
|
358
361
|
except Exception as e:
|
359
362
|
logger.error(f"Error importing profiles from {file_path}: {e}")
|
360
363
|
return results
|
364
|
+
|
365
|
+
def _create_credential_source_from_legacy(
|
366
|
+
self,
|
367
|
+
auth_mode: Optional[str],
|
368
|
+
client_id: Optional[str],
|
369
|
+
client_secret: Optional[str],
|
370
|
+
tenant_id: Optional[str]
|
371
|
+
) -> Optional["CredentialSource"]:
|
372
|
+
"""Create credential source from legacy credential parameters.
|
373
|
+
|
374
|
+
Args:
|
375
|
+
auth_mode: Legacy authentication mode
|
376
|
+
client_id: Legacy client ID
|
377
|
+
client_secret: Legacy client secret
|
378
|
+
tenant_id: Legacy tenant ID
|
379
|
+
|
380
|
+
Returns:
|
381
|
+
CredentialSource instance or None for default credentials
|
382
|
+
"""
|
383
|
+
# If auth_mode is explicitly "default" or no credentials provided, use default credentials
|
384
|
+
if auth_mode == "default" or not any([client_id, client_secret, tenant_id]):
|
385
|
+
return None
|
386
|
+
|
387
|
+
# If we have explicit credentials, create environment credential source
|
388
|
+
if all([client_id, client_secret, tenant_id]):
|
389
|
+
from .credential_sources import EnvironmentCredentialSource
|
390
|
+
logger.info("Converting legacy credentials to environment credential source")
|
391
|
+
return EnvironmentCredentialSource()
|
392
|
+
|
393
|
+
# If we have partial credentials, log warning and use default
|
394
|
+
logger.warning("Incomplete legacy credentials provided, using default credentials")
|
395
|
+
return None
|