d365fo-client 0.1.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 +305 -0
- d365fo_client/auth.py +93 -0
- d365fo_client/cli.py +700 -0
- d365fo_client/client.py +1454 -0
- d365fo_client/config.py +304 -0
- d365fo_client/crud.py +200 -0
- d365fo_client/exceptions.py +49 -0
- d365fo_client/labels.py +528 -0
- d365fo_client/main.py +502 -0
- d365fo_client/mcp/__init__.py +16 -0
- d365fo_client/mcp/client_manager.py +276 -0
- d365fo_client/mcp/main.py +98 -0
- d365fo_client/mcp/models.py +371 -0
- d365fo_client/mcp/prompts/__init__.py +43 -0
- d365fo_client/mcp/prompts/action_execution.py +480 -0
- d365fo_client/mcp/prompts/sequence_analysis.py +349 -0
- d365fo_client/mcp/resources/__init__.py +15 -0
- d365fo_client/mcp/resources/database_handler.py +555 -0
- d365fo_client/mcp/resources/entity_handler.py +176 -0
- d365fo_client/mcp/resources/environment_handler.py +132 -0
- d365fo_client/mcp/resources/metadata_handler.py +283 -0
- d365fo_client/mcp/resources/query_handler.py +135 -0
- d365fo_client/mcp/server.py +432 -0
- d365fo_client/mcp/tools/__init__.py +17 -0
- d365fo_client/mcp/tools/connection_tools.py +175 -0
- d365fo_client/mcp/tools/crud_tools.py +579 -0
- d365fo_client/mcp/tools/database_tools.py +813 -0
- d365fo_client/mcp/tools/label_tools.py +189 -0
- d365fo_client/mcp/tools/metadata_tools.py +766 -0
- d365fo_client/mcp/tools/profile_tools.py +706 -0
- d365fo_client/metadata_api.py +793 -0
- d365fo_client/metadata_v2/__init__.py +59 -0
- d365fo_client/metadata_v2/cache_v2.py +1372 -0
- d365fo_client/metadata_v2/database_v2.py +585 -0
- d365fo_client/metadata_v2/global_version_manager.py +573 -0
- d365fo_client/metadata_v2/search_engine_v2.py +423 -0
- d365fo_client/metadata_v2/sync_manager_v2.py +819 -0
- d365fo_client/metadata_v2/version_detector.py +439 -0
- d365fo_client/models.py +862 -0
- d365fo_client/output.py +181 -0
- d365fo_client/profile_manager.py +342 -0
- d365fo_client/profiles.py +178 -0
- d365fo_client/query.py +162 -0
- d365fo_client/session.py +60 -0
- d365fo_client/utils.py +196 -0
- d365fo_client-0.1.0.dist-info/METADATA +1084 -0
- d365fo_client-0.1.0.dist-info/RECORD +51 -0
- d365fo_client-0.1.0.dist-info/WHEEL +5 -0
- d365fo_client-0.1.0.dist-info/entry_points.txt +3 -0
- d365fo_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- d365fo_client-0.1.0.dist-info/top_level.txt +1 -0
d365fo_client/config.py
ADDED
@@ -0,0 +1,304 @@
|
|
1
|
+
"""Configuration management for d365fo-client CLI."""
|
2
|
+
|
3
|
+
import argparse
|
4
|
+
import logging
|
5
|
+
import os
|
6
|
+
from dataclasses import asdict, dataclass
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Any, Dict, Optional
|
9
|
+
|
10
|
+
import yaml
|
11
|
+
|
12
|
+
from .models import FOClientConfig
|
13
|
+
from .profiles import Profile
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
# Legacy alias for backward compatibility
|
19
|
+
CLIProfile = Profile
|
20
|
+
|
21
|
+
|
22
|
+
class ConfigManager:
|
23
|
+
"""Manages configuration profiles and settings."""
|
24
|
+
|
25
|
+
def __init__(self, config_path: Optional[str] = None):
|
26
|
+
"""Initialize configuration manager.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
config_path: Path to configuration file. If None, uses default.
|
30
|
+
"""
|
31
|
+
self.config_path = config_path or self._get_default_config_path()
|
32
|
+
self._config_data = self._load_config()
|
33
|
+
|
34
|
+
def _get_default_config_path(self) -> str:
|
35
|
+
"""Get the default configuration file path."""
|
36
|
+
config_dir = Path.home() / ".d365fo-client"
|
37
|
+
config_dir.mkdir(exist_ok=True)
|
38
|
+
return str(config_dir / "config.yaml")
|
39
|
+
|
40
|
+
def _load_config(self) -> Dict[str, Any]:
|
41
|
+
"""Load configuration from file."""
|
42
|
+
if not os.path.exists(self.config_path):
|
43
|
+
return {"profiles": {}, "default_profile": None, "global": {}}
|
44
|
+
|
45
|
+
try:
|
46
|
+
with open(self.config_path, "r", encoding="utf-8") as f:
|
47
|
+
config = yaml.safe_load(f) or {}
|
48
|
+
|
49
|
+
# Ensure required sections exist
|
50
|
+
if "profiles" not in config:
|
51
|
+
config["profiles"] = {}
|
52
|
+
if "global" not in config:
|
53
|
+
config["global"] = {}
|
54
|
+
|
55
|
+
return config
|
56
|
+
except Exception as e:
|
57
|
+
# If config file is corrupted, return empty config
|
58
|
+
print(f"Warning: Could not load config file {self.config_path}: {e}")
|
59
|
+
return {"profiles": {}, "default_profile": None, "global": {}}
|
60
|
+
|
61
|
+
def _save_config(self) -> None:
|
62
|
+
"""Save configuration to file."""
|
63
|
+
try:
|
64
|
+
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
|
65
|
+
with open(self.config_path, "w", encoding="utf-8") as f:
|
66
|
+
yaml.dump(
|
67
|
+
self._config_data, f, default_flow_style=False, allow_unicode=True
|
68
|
+
)
|
69
|
+
except Exception as e:
|
70
|
+
print(f"Error saving config file {self.config_path}: {e}")
|
71
|
+
|
72
|
+
def get_profile(self, profile_name: str) -> Optional[Profile]:
|
73
|
+
"""Get a specific configuration profile.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
profile_name: Name of the profile to retrieve
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
Profile instance or None if not found
|
80
|
+
"""
|
81
|
+
profiles = self._config_data.get("profiles", {})
|
82
|
+
if profile_name not in profiles:
|
83
|
+
return None
|
84
|
+
|
85
|
+
profile_data = profiles[profile_name]
|
86
|
+
try:
|
87
|
+
return Profile.from_dict(profile_name, profile_data)
|
88
|
+
except Exception as e:
|
89
|
+
logger.error(f"Error loading profile {profile_name}: {e}")
|
90
|
+
return None
|
91
|
+
|
92
|
+
def save_profile(self, profile: Profile) -> None:
|
93
|
+
"""Save a configuration profile.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
profile: Profile to save
|
97
|
+
"""
|
98
|
+
if "profiles" not in self._config_data:
|
99
|
+
self._config_data["profiles"] = {}
|
100
|
+
|
101
|
+
# Convert profile to dict for storage
|
102
|
+
self._config_data["profiles"][profile.name] = profile.to_dict()
|
103
|
+
self._save_config()
|
104
|
+
|
105
|
+
def delete_profile(self, profile_name: str) -> bool:
|
106
|
+
"""Delete a configuration profile.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
profile_name: Name of the profile to delete
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
True if deleted, False if not found
|
113
|
+
"""
|
114
|
+
profiles = self._config_data.get("profiles", {})
|
115
|
+
if profile_name not in profiles:
|
116
|
+
return False
|
117
|
+
|
118
|
+
del profiles[profile_name]
|
119
|
+
|
120
|
+
# Clear default if it was the deleted profile
|
121
|
+
if self._config_data.get("default_profile") == profile_name:
|
122
|
+
self._config_data["default_profile"] = None
|
123
|
+
|
124
|
+
self._save_config()
|
125
|
+
return True
|
126
|
+
|
127
|
+
def list_profiles(self) -> Dict[str, Profile]:
|
128
|
+
"""List all configuration profiles.
|
129
|
+
|
130
|
+
Returns:
|
131
|
+
Dictionary of profile name to Profile instances
|
132
|
+
"""
|
133
|
+
profiles = {}
|
134
|
+
for name, data in self._config_data.get("profiles", {}).items():
|
135
|
+
try:
|
136
|
+
profiles[name] = Profile.from_dict(name, data)
|
137
|
+
except Exception as e:
|
138
|
+
logger.error(f"Error loading profile {name}: {e}")
|
139
|
+
continue
|
140
|
+
return profiles
|
141
|
+
|
142
|
+
def set_default_profile(self, profile_name: str) -> bool:
|
143
|
+
"""Set the default profile.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
profile_name: Name of the profile to set as default
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
True if set, False if profile doesn't exist
|
150
|
+
"""
|
151
|
+
profiles = self._config_data.get("profiles", {})
|
152
|
+
if profile_name not in profiles:
|
153
|
+
return False
|
154
|
+
|
155
|
+
self._config_data["default_profile"] = profile_name
|
156
|
+
self._save_config()
|
157
|
+
return True
|
158
|
+
|
159
|
+
def get_default_profile(self) -> Optional[Profile]:
|
160
|
+
"""Get the default configuration profile.
|
161
|
+
|
162
|
+
Returns:
|
163
|
+
Default Profile instance or None if not set
|
164
|
+
"""
|
165
|
+
default_name = self._config_data.get("default_profile")
|
166
|
+
if not default_name:
|
167
|
+
return None
|
168
|
+
|
169
|
+
return self.get_profile(default_name)
|
170
|
+
|
171
|
+
def get_effective_config(self, args: argparse.Namespace) -> FOClientConfig:
|
172
|
+
"""Build effective configuration from args, environment, and profiles.
|
173
|
+
|
174
|
+
Precedence: Command line args > Environment variables > Profile config > Defaults
|
175
|
+
|
176
|
+
Args:
|
177
|
+
args: Parsed command line arguments
|
178
|
+
|
179
|
+
Returns:
|
180
|
+
FOClientConfig instance
|
181
|
+
"""
|
182
|
+
# Start with defaults
|
183
|
+
config_params = {
|
184
|
+
"base_url": None,
|
185
|
+
"use_default_credentials": True,
|
186
|
+
"client_id": None,
|
187
|
+
"client_secret": None,
|
188
|
+
"tenant_id": None,
|
189
|
+
"verify_ssl": True,
|
190
|
+
"use_label_cache": True,
|
191
|
+
"label_cache_expiry_minutes": 60,
|
192
|
+
"use_cache_first": True,
|
193
|
+
"timeout": 60,
|
194
|
+
}
|
195
|
+
|
196
|
+
# Apply profile settings if specified
|
197
|
+
profile = None
|
198
|
+
profile_name = getattr(args, "profile", None)
|
199
|
+
if profile_name:
|
200
|
+
profile = self.get_profile(profile_name)
|
201
|
+
elif not profile_name:
|
202
|
+
# Try default profile
|
203
|
+
profile = self.get_default_profile()
|
204
|
+
|
205
|
+
if profile:
|
206
|
+
config_params.update(
|
207
|
+
{
|
208
|
+
"base_url": profile.base_url,
|
209
|
+
"client_id": profile.client_id,
|
210
|
+
"client_secret": profile.client_secret,
|
211
|
+
"tenant_id": profile.tenant_id,
|
212
|
+
"verify_ssl": profile.verify_ssl,
|
213
|
+
"use_label_cache": profile.use_label_cache,
|
214
|
+
"label_cache_expiry_minutes": profile.label_cache_expiry_minutes,
|
215
|
+
"use_cache_first": profile.use_cache_first,
|
216
|
+
"timeout": profile.timeout,
|
217
|
+
}
|
218
|
+
)
|
219
|
+
|
220
|
+
# Apply environment variables
|
221
|
+
env_mappings = {
|
222
|
+
"D365FO_BASE_URL": "base_url",
|
223
|
+
"D365FO_CLIENT_ID": "client_id",
|
224
|
+
"D365FO_CLIENT_SECRET": "client_secret",
|
225
|
+
"D365FO_TENANT_ID": "tenant_id",
|
226
|
+
"D365FO_VERIFY_SSL": "verify_ssl",
|
227
|
+
"D365FO_LABEL_CACHE": "use_label_cache",
|
228
|
+
"D365FO_LABEL_EXPIRY": "label_cache_expiry_minutes",
|
229
|
+
"D365FO_USE_CACHE_FIRST": "use_cache_first",
|
230
|
+
"D365FO_TIMEOUT": "timeout",
|
231
|
+
}
|
232
|
+
|
233
|
+
for env_var, param_name in env_mappings.items():
|
234
|
+
env_value = os.getenv(env_var)
|
235
|
+
if env_value:
|
236
|
+
if param_name in ["verify_ssl", "use_label_cache", "use_cache_first"]:
|
237
|
+
# Convert to boolean
|
238
|
+
config_params[param_name] = env_value.lower() in (
|
239
|
+
"true",
|
240
|
+
"1",
|
241
|
+
"yes",
|
242
|
+
"on",
|
243
|
+
)
|
244
|
+
elif param_name in ["label_cache_expiry_minutes", "timeout"]:
|
245
|
+
# Convert to int
|
246
|
+
try:
|
247
|
+
config_params[param_name] = int(env_value)
|
248
|
+
except ValueError:
|
249
|
+
pass # Keep default/profile value
|
250
|
+
else:
|
251
|
+
config_params[param_name] = env_value
|
252
|
+
|
253
|
+
# Apply command line arguments (highest precedence)
|
254
|
+
arg_mappings = {
|
255
|
+
"base_url": "base_url",
|
256
|
+
"client_id": "client_id",
|
257
|
+
"client_secret": "client_secret",
|
258
|
+
"tenant_id": "tenant_id",
|
259
|
+
"verify_ssl": "verify_ssl",
|
260
|
+
"label_cache": "use_label_cache",
|
261
|
+
"label_expiry": "label_cache_expiry_minutes",
|
262
|
+
"use_cache_first": "use_cache_first",
|
263
|
+
"timeout": "timeout",
|
264
|
+
}
|
265
|
+
|
266
|
+
for arg_name, param_name in arg_mappings.items():
|
267
|
+
arg_value = getattr(args, arg_name, None)
|
268
|
+
if arg_value is not None:
|
269
|
+
config_params[param_name] = arg_value
|
270
|
+
|
271
|
+
# Determine authentication mode
|
272
|
+
if any(
|
273
|
+
[
|
274
|
+
config_params["client_id"],
|
275
|
+
config_params["client_secret"],
|
276
|
+
config_params["tenant_id"],
|
277
|
+
]
|
278
|
+
):
|
279
|
+
config_params["use_default_credentials"] = False
|
280
|
+
|
281
|
+
return FOClientConfig(**config_params)
|
282
|
+
|
283
|
+
def _substitute_env_variables(self, value: Any) -> Any:
|
284
|
+
"""Substitute environment variables in configuration values.
|
285
|
+
|
286
|
+
Supports ${VAR_NAME} syntax.
|
287
|
+
|
288
|
+
Args:
|
289
|
+
value: Value to process
|
290
|
+
|
291
|
+
Returns:
|
292
|
+
Value with environment variables substituted
|
293
|
+
"""
|
294
|
+
if not isinstance(value, str):
|
295
|
+
return value
|
296
|
+
|
297
|
+
# Simple environment variable substitution
|
298
|
+
import re
|
299
|
+
|
300
|
+
def replace_var(match):
|
301
|
+
var_name = match.group(1)
|
302
|
+
return os.getenv(var_name, match.group(0)) # Return original if not found
|
303
|
+
|
304
|
+
return re.sub(r"\$\{([^}]+)\}", replace_var, value)
|
d365fo_client/crud.py
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
"""CRUD operations for D365 F&O client."""
|
2
|
+
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
4
|
+
|
5
|
+
import aiohttp
|
6
|
+
|
7
|
+
from .models import QueryOptions
|
8
|
+
from .query import QueryBuilder
|
9
|
+
from .session import SessionManager
|
10
|
+
|
11
|
+
|
12
|
+
class CrudOperations:
|
13
|
+
"""Handles CRUD operations for F&O entities"""
|
14
|
+
|
15
|
+
def __init__(self, session_manager: SessionManager, base_url: str):
|
16
|
+
"""Initialize CRUD operations
|
17
|
+
|
18
|
+
Args:
|
19
|
+
session_manager: HTTP session manager
|
20
|
+
base_url: Base F&O URL
|
21
|
+
"""
|
22
|
+
self.session_manager = session_manager
|
23
|
+
self.base_url = base_url
|
24
|
+
|
25
|
+
async def get_entities(
|
26
|
+
self, entity_name: str, options: Optional[QueryOptions] = None
|
27
|
+
) -> Dict[str, Any]:
|
28
|
+
"""Get entities with OData query options
|
29
|
+
|
30
|
+
Args:
|
31
|
+
entity_name: Name of the entity set
|
32
|
+
options: OData query options
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
Response containing entities
|
36
|
+
"""
|
37
|
+
session = await self.session_manager.get_session()
|
38
|
+
query_string = QueryBuilder.build_query_string(options)
|
39
|
+
url = f"{self.base_url}/data/{entity_name}{query_string}"
|
40
|
+
|
41
|
+
async with session.get(url) as response:
|
42
|
+
if response.status == 200:
|
43
|
+
return await response.json()
|
44
|
+
else:
|
45
|
+
error_text = await response.text()
|
46
|
+
raise Exception(
|
47
|
+
f"GET {entity_name} failed: {response.status} - {error_text}"
|
48
|
+
)
|
49
|
+
|
50
|
+
async def get_entity(
|
51
|
+
self,
|
52
|
+
entity_name: str,
|
53
|
+
key: Union[str, Dict[str, Any]],
|
54
|
+
options: Optional[QueryOptions] = None,
|
55
|
+
) -> Dict[str, Any]:
|
56
|
+
"""Get single entity by key
|
57
|
+
|
58
|
+
Args:
|
59
|
+
entity_name: Name of the entity set
|
60
|
+
key: Entity key value (string for simple keys, dict for composite keys)
|
61
|
+
options: OData query options
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
Entity data
|
65
|
+
"""
|
66
|
+
session = await self.session_manager.get_session()
|
67
|
+
query_string = QueryBuilder.build_query_string(options)
|
68
|
+
url = QueryBuilder.build_entity_url(self.base_url, entity_name, key)
|
69
|
+
url += query_string
|
70
|
+
|
71
|
+
async with session.get(url) as response:
|
72
|
+
if response.status == 200:
|
73
|
+
return await response.json()
|
74
|
+
else:
|
75
|
+
error_text = await response.text()
|
76
|
+
raise Exception(
|
77
|
+
f"GET {entity_name}({key}) failed: {response.status} - {error_text}"
|
78
|
+
)
|
79
|
+
|
80
|
+
async def create_entity(
|
81
|
+
self, entity_name: str, data: Dict[str, Any]
|
82
|
+
) -> Dict[str, Any]:
|
83
|
+
"""Create new entity
|
84
|
+
|
85
|
+
Args:
|
86
|
+
entity_name: Name of the entity set
|
87
|
+
data: Entity data to create
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
Created entity data
|
91
|
+
"""
|
92
|
+
session = await self.session_manager.get_session()
|
93
|
+
url = f"{self.base_url}/data/{entity_name}"
|
94
|
+
|
95
|
+
async with session.post(url, json=data) as response:
|
96
|
+
if response.status in [200, 201]:
|
97
|
+
return await response.json()
|
98
|
+
else:
|
99
|
+
error_text = await response.text()
|
100
|
+
raise Exception(
|
101
|
+
f"CREATE {entity_name} failed: {response.status} - {error_text}"
|
102
|
+
)
|
103
|
+
|
104
|
+
async def update_entity(
|
105
|
+
self,
|
106
|
+
entity_name: str,
|
107
|
+
key: Union[str, Dict[str, Any]],
|
108
|
+
data: Dict[str, Any],
|
109
|
+
method: str = "PATCH",
|
110
|
+
) -> Dict[str, Any]:
|
111
|
+
"""Update existing entity
|
112
|
+
|
113
|
+
Args:
|
114
|
+
entity_name: Name of the entity set
|
115
|
+
key: Entity key value (string for simple keys, dict for composite keys)
|
116
|
+
data: Updated entity data
|
117
|
+
method: HTTP method (PATCH or PUT)
|
118
|
+
|
119
|
+
Returns:
|
120
|
+
Updated entity data
|
121
|
+
"""
|
122
|
+
session = await self.session_manager.get_session()
|
123
|
+
url = QueryBuilder.build_entity_url(self.base_url, entity_name, key)
|
124
|
+
|
125
|
+
async with session.request(method, url, json=data) as response:
|
126
|
+
if response.status in [200, 204]:
|
127
|
+
if response.status == 204:
|
128
|
+
return {"success": True}
|
129
|
+
return await response.json()
|
130
|
+
else:
|
131
|
+
error_text = await response.text()
|
132
|
+
raise Exception(
|
133
|
+
f"{method} {entity_name}({key}) failed: {response.status} - {error_text}"
|
134
|
+
)
|
135
|
+
|
136
|
+
async def delete_entity(
|
137
|
+
self, entity_name: str, key: Union[str, Dict[str, Any]]
|
138
|
+
) -> bool:
|
139
|
+
"""Delete entity
|
140
|
+
|
141
|
+
Args:
|
142
|
+
entity_name: Name of the entity set
|
143
|
+
key: Entity key value (string for simple keys, dict for composite keys)
|
144
|
+
|
145
|
+
Returns:
|
146
|
+
True if successful
|
147
|
+
"""
|
148
|
+
session = await self.session_manager.get_session()
|
149
|
+
url = QueryBuilder.build_entity_url(self.base_url, entity_name, key)
|
150
|
+
|
151
|
+
async with session.delete(url) as response:
|
152
|
+
if response.status in [200, 204]:
|
153
|
+
return True
|
154
|
+
else:
|
155
|
+
error_text = await response.text()
|
156
|
+
raise Exception(
|
157
|
+
f"DELETE {entity_name}({key}) failed: {response.status} - {error_text}"
|
158
|
+
)
|
159
|
+
|
160
|
+
async def call_action(
|
161
|
+
self,
|
162
|
+
action_name: str,
|
163
|
+
parameters: Optional[Dict[str, Any]] = None,
|
164
|
+
entity_name: Optional[str] = None,
|
165
|
+
entity_key: Optional[Union[str, Dict[str, Any]]] = None,
|
166
|
+
) -> Any:
|
167
|
+
"""Call OData action method
|
168
|
+
|
169
|
+
Args:
|
170
|
+
action_name: Name of the action
|
171
|
+
parameters: Action parameters
|
172
|
+
entity_name: Entity name for bound actions
|
173
|
+
entity_key: Entity key for bound actions (string for simple keys, dict for composite keys)
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
Action result
|
177
|
+
"""
|
178
|
+
session = await self.session_manager.get_session()
|
179
|
+
url = QueryBuilder.build_action_url(
|
180
|
+
self.base_url, action_name, entity_name, entity_key
|
181
|
+
)
|
182
|
+
|
183
|
+
# Prepare request body
|
184
|
+
body = parameters or {}
|
185
|
+
|
186
|
+
async with session.post(url, json=body) as response:
|
187
|
+
if response.status in [200, 201, 204]:
|
188
|
+
if response.status == 204:
|
189
|
+
return {"success": True}
|
190
|
+
|
191
|
+
content_type = response.headers.get("content-type", "")
|
192
|
+
if "application/json" in content_type:
|
193
|
+
return await response.json()
|
194
|
+
else:
|
195
|
+
return await response.text()
|
196
|
+
else:
|
197
|
+
error_text = await response.text()
|
198
|
+
raise Exception(
|
199
|
+
f"Action {action_name} failed: {response.status} - {error_text}"
|
200
|
+
)
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"""Exception classes for D365 F&O client."""
|
2
|
+
|
3
|
+
|
4
|
+
class FOClientError(Exception):
|
5
|
+
"""Base exception for F&O client errors"""
|
6
|
+
|
7
|
+
pass
|
8
|
+
|
9
|
+
|
10
|
+
class AuthenticationError(FOClientError):
|
11
|
+
"""Authentication related errors"""
|
12
|
+
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
class MetadataError(FOClientError):
|
17
|
+
"""Metadata operation errors"""
|
18
|
+
|
19
|
+
pass
|
20
|
+
|
21
|
+
|
22
|
+
class EntityError(FOClientError):
|
23
|
+
"""Entity operation errors"""
|
24
|
+
|
25
|
+
pass
|
26
|
+
|
27
|
+
|
28
|
+
class ActionError(FOClientError):
|
29
|
+
"""Action execution errors"""
|
30
|
+
|
31
|
+
pass
|
32
|
+
|
33
|
+
|
34
|
+
class LabelError(FOClientError):
|
35
|
+
"""Label operation errors"""
|
36
|
+
|
37
|
+
pass
|
38
|
+
|
39
|
+
|
40
|
+
class ConfigurationError(FOClientError):
|
41
|
+
"""Configuration related errors"""
|
42
|
+
|
43
|
+
pass
|
44
|
+
|
45
|
+
|
46
|
+
class NetworkError(FOClientError):
|
47
|
+
"""Network and HTTP related errors"""
|
48
|
+
|
49
|
+
pass
|