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.
Files changed (51) hide show
  1. d365fo_client/__init__.py +305 -0
  2. d365fo_client/auth.py +93 -0
  3. d365fo_client/cli.py +700 -0
  4. d365fo_client/client.py +1454 -0
  5. d365fo_client/config.py +304 -0
  6. d365fo_client/crud.py +200 -0
  7. d365fo_client/exceptions.py +49 -0
  8. d365fo_client/labels.py +528 -0
  9. d365fo_client/main.py +502 -0
  10. d365fo_client/mcp/__init__.py +16 -0
  11. d365fo_client/mcp/client_manager.py +276 -0
  12. d365fo_client/mcp/main.py +98 -0
  13. d365fo_client/mcp/models.py +371 -0
  14. d365fo_client/mcp/prompts/__init__.py +43 -0
  15. d365fo_client/mcp/prompts/action_execution.py +480 -0
  16. d365fo_client/mcp/prompts/sequence_analysis.py +349 -0
  17. d365fo_client/mcp/resources/__init__.py +15 -0
  18. d365fo_client/mcp/resources/database_handler.py +555 -0
  19. d365fo_client/mcp/resources/entity_handler.py +176 -0
  20. d365fo_client/mcp/resources/environment_handler.py +132 -0
  21. d365fo_client/mcp/resources/metadata_handler.py +283 -0
  22. d365fo_client/mcp/resources/query_handler.py +135 -0
  23. d365fo_client/mcp/server.py +432 -0
  24. d365fo_client/mcp/tools/__init__.py +17 -0
  25. d365fo_client/mcp/tools/connection_tools.py +175 -0
  26. d365fo_client/mcp/tools/crud_tools.py +579 -0
  27. d365fo_client/mcp/tools/database_tools.py +813 -0
  28. d365fo_client/mcp/tools/label_tools.py +189 -0
  29. d365fo_client/mcp/tools/metadata_tools.py +766 -0
  30. d365fo_client/mcp/tools/profile_tools.py +706 -0
  31. d365fo_client/metadata_api.py +793 -0
  32. d365fo_client/metadata_v2/__init__.py +59 -0
  33. d365fo_client/metadata_v2/cache_v2.py +1372 -0
  34. d365fo_client/metadata_v2/database_v2.py +585 -0
  35. d365fo_client/metadata_v2/global_version_manager.py +573 -0
  36. d365fo_client/metadata_v2/search_engine_v2.py +423 -0
  37. d365fo_client/metadata_v2/sync_manager_v2.py +819 -0
  38. d365fo_client/metadata_v2/version_detector.py +439 -0
  39. d365fo_client/models.py +862 -0
  40. d365fo_client/output.py +181 -0
  41. d365fo_client/profile_manager.py +342 -0
  42. d365fo_client/profiles.py +178 -0
  43. d365fo_client/query.py +162 -0
  44. d365fo_client/session.py +60 -0
  45. d365fo_client/utils.py +196 -0
  46. d365fo_client-0.1.0.dist-info/METADATA +1084 -0
  47. d365fo_client-0.1.0.dist-info/RECORD +51 -0
  48. d365fo_client-0.1.0.dist-info/WHEEL +5 -0
  49. d365fo_client-0.1.0.dist-info/entry_points.txt +3 -0
  50. d365fo_client-0.1.0.dist-info/licenses/LICENSE +21 -0
  51. d365fo_client-0.1.0.dist-info/top_level.txt +1 -0
d365fo_client/main.py ADDED
@@ -0,0 +1,502 @@
1
+ """Main module for d365fo-client package with example usage."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import sys
6
+
7
+ from .client import FOClient, create_client
8
+ from .models import FOClientConfig, QueryOptions
9
+
10
+
11
+ async def example_usage():
12
+ """Example usage of the F&O client with label functionality"""
13
+ config = FOClientConfig(
14
+ base_url="https://usnconeboxax1aos.cloud.onebox.dynamics.com",
15
+ use_default_credentials=True,
16
+ verify_ssl=False,
17
+ use_label_cache=True,
18
+ label_cache_expiry_minutes=60,
19
+ )
20
+
21
+ async with FOClient(config) as client:
22
+ # Test connections
23
+ print("šŸ”— Testing connections...")
24
+ if await client.test_connection():
25
+ print("āœ… Connected to F&O OData successfully")
26
+
27
+ if await client.test_metadata_connection():
28
+ print("āœ… Connected to F&O Metadata API successfully")
29
+
30
+ # Download metadata
31
+ print("\nšŸ“„ Downloading metadata...")
32
+ await client.download_metadata()
33
+
34
+ # Search entities
35
+ print("\nšŸ” Searching entities...")
36
+ customer_entities = await client.search_entities("customer")
37
+ print(f"Found {len(customer_entities)} customer-related entities")
38
+ for entity in customer_entities[:5]: # Show first 5
39
+ print(f" - {entity}")
40
+
41
+ # Get entity info with labels
42
+ print("\nšŸ“Š Getting entity information...")
43
+ customers_info = await client.get_entity_info_with_labels("Customer")
44
+ if customers_info:
45
+ print(f"Customers entity: {customers_info.name}")
46
+ if customers_info.label_text:
47
+ print(f"Entity label: '{customers_info.label_text}'")
48
+ print(f"Has {len(customers_info.properties)} properties")
49
+
50
+ # Show properties with labels
51
+ labeled_props = [p for p in customers_info.properties if p.label_text][:5]
52
+ if labeled_props:
53
+ print("Properties with labels:")
54
+ for prop in labeled_props:
55
+ print(f" {prop.name}: '{prop.label_text}'")
56
+
57
+ # Test label operations
58
+ print("\nšŸ·ļø Label Operations:")
59
+
60
+ # Get specific labels
61
+ test_labels = ["@SYS78125", "@SYS9490", "@GLS63332"]
62
+ print("Fetching specific labels:")
63
+ for label_id in test_labels:
64
+ text = await client.get_label_text(label_id)
65
+ print(f" {label_id}: '{text}'")
66
+
67
+ # Show cache info
68
+ cache_info = client.get_label_cache_info()
69
+ print(f"Label cache: {cache_info}")
70
+
71
+ # Get entities with query options
72
+ print("\nšŸ“‹ Querying entities...")
73
+ query_options = QueryOptions(
74
+ select=["CustomerAccount", "Name", "SalesCurrencyCode"],
75
+ top=5,
76
+ orderby=["Name"],
77
+ )
78
+
79
+ try:
80
+ customers = await client.get_entities("Customers", query_options)
81
+ print(f"Retrieved {len(customers.get('value', []))} customers")
82
+ for customer in customers.get("value", [])[:3]: # Show first 3
83
+ print(f" - {customer.get('CustomerAccount')}: {customer.get('Name')}")
84
+ except Exception as e:
85
+ print(f"Error querying customers: {e}")
86
+
87
+ # Search and call actions
88
+ print("\n⚔ Searching actions...")
89
+ calc_actions = await client.search_actions("calculate")
90
+ print(f"Found {len(calc_actions)} calculation actions")
91
+ for action in calc_actions[:5]: # Show first 5
92
+ print(f" - {action}")
93
+
94
+ print("\nšŸ”§ Calling actions...")
95
+
96
+ # Use the new dedicated methods for version information
97
+ print("Getting version information using dedicated methods...")
98
+ try:
99
+ app_version = await client.get_application_version()
100
+ print(f"Application Version: {app_version}")
101
+ except Exception as e:
102
+ print(f"Error getting application version: {e}")
103
+
104
+ try:
105
+ platform_build_version = await client.get_platform_build_version()
106
+ print(f"Platform Build Version: {platform_build_version}")
107
+ except Exception as e:
108
+ print(f"Error getting platform build version: {e}")
109
+
110
+ try:
111
+ app_build_version = await client.get_application_build_version()
112
+ print(f"Application Build Version: {app_build_version}")
113
+ except Exception as e:
114
+ print(f"Error getting application build version: {e}")
115
+
116
+ # Call other actions using generic call_action
117
+ entity_actions = {
118
+ "DocumentRoutingClientApps": ["GetPlatformVersion"],
119
+ }
120
+
121
+ for entity in entity_actions:
122
+ for action in entity_actions[entity]:
123
+ print(f"Calling action '{action}' on entity '{entity}'...")
124
+ result = await client.call_action(action, entity_name=entity)
125
+ print(f"Action '{action}' result: {result}")
126
+
127
+ # New Metadata APIs demonstration
128
+ print("\nšŸ†• New Metadata APIs:")
129
+
130
+ # Data Entities API
131
+ print("\nšŸ“Š Data Entities API:")
132
+ data_entities = await client.search_data_entities(
133
+ "customer", entity_category="Master"
134
+ )
135
+ print(f"Found {len(data_entities)} customer Master data entities")
136
+ if data_entities:
137
+ entity = data_entities[0]
138
+ print(f" Example: {entity.name} -> {entity.public_collection_name}")
139
+ print(f" Category: {entity.entity_category}")
140
+ print(f" Label: {entity.label_text or entity.label_id}")
141
+
142
+ # Public Entities API
143
+ print("\nšŸ“‹ Public Entities API:")
144
+ public_entities = await client.search_public_entities("customer")
145
+ print(f"Found {len(public_entities)} customer public entities")
146
+ if public_entities:
147
+ # Get detailed info for first entity
148
+ entity_detail = await client.get_public_entity_info(public_entities[0].name)
149
+ if entity_detail:
150
+ print(
151
+ f" {entity_detail.name}: {len(entity_detail.properties)} properties"
152
+ )
153
+ key_props = [p.name for p in entity_detail.properties if p.is_key]
154
+ print(f" Keys: {', '.join(key_props)}")
155
+
156
+ # Public Enumerations API
157
+ print("\nšŸ”¢ Public Enumerations API:")
158
+ enumerations = await client.search_public_enumerations("payment")
159
+ print(f"Found {len(enumerations)} payment-related enumerations")
160
+ if enumerations:
161
+ # Get detailed info for first enumeration
162
+ enum_detail = await client.get_public_enumeration_info(enumerations[0].name)
163
+ if enum_detail:
164
+ print(f" {enum_detail.name}: {len(enum_detail.members)} values")
165
+ print(f" Label: {enum_detail.label_text or enum_detail.label_id}")
166
+ if enum_detail.members:
167
+ print(
168
+ f" Sample values: {', '.join([f'{m.name}={m.value}' for m in enum_detail.members[:3]])}"
169
+ )
170
+
171
+
172
+ def create_argument_parser() -> argparse.ArgumentParser:
173
+ """Create the enhanced argument parser with all CLI commands."""
174
+ from . import __author__, __email__, __version__
175
+
176
+ parser = argparse.ArgumentParser(
177
+ description="Microsoft Dynamics 365 Finance & Operations Client",
178
+ prog="d365fo-client",
179
+ )
180
+
181
+ # Global options (available for all commands)
182
+ parser.add_argument(
183
+ "--version",
184
+ action="version",
185
+ version=f"d365fo-client {__version__} by {__author__} ({__email__})",
186
+ )
187
+ parser.add_argument("--base-url", help="D365 F&O environment URL")
188
+ parser.add_argument(
189
+ "--auth-mode",
190
+ choices=["default", "explicit", "interactive"],
191
+ default="default",
192
+ help="Authentication mode (default: default)",
193
+ )
194
+ parser.add_argument("--client-id", help="Azure AD client ID")
195
+ parser.add_argument("--client-secret", help="Azure AD client secret")
196
+ parser.add_argument("--tenant-id", help="Azure AD tenant ID")
197
+ parser.add_argument(
198
+ "--verify-ssl", type=bool, default=True, help="SSL verification (default: true)"
199
+ )
200
+ parser.add_argument(
201
+ "--output",
202
+ choices=["json", "table", "csv", "yaml"],
203
+ default="table",
204
+ help="Output format (default: table)",
205
+ )
206
+ parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
207
+ parser.add_argument(
208
+ "--quiet", "-q", action="store_true", help="Suppress non-essential output"
209
+ )
210
+ parser.add_argument("--config", help="Configuration file path")
211
+ parser.add_argument("--profile", help="Configuration profile to use")
212
+ parser.add_argument(
213
+ "--label-cache",
214
+ type=bool,
215
+ default=True,
216
+ help="Enable label caching (default: true)",
217
+ )
218
+ parser.add_argument(
219
+ "--label-expiry",
220
+ type=int,
221
+ default=60,
222
+ help="Label cache expiry in minutes (default: 60)",
223
+ )
224
+
225
+ # Legacy option for backward compatibility
226
+ parser.add_argument(
227
+ "--demo", action="store_true", help="Run the demo/example usage"
228
+ )
229
+
230
+ # Subcommands
231
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
232
+
233
+ # Add command subparsers
234
+ _add_test_command(subparsers)
235
+ _add_version_command(subparsers)
236
+ _add_metadata_commands(subparsers)
237
+ _add_entity_commands(subparsers)
238
+ _add_action_commands(subparsers)
239
+ _add_config_commands(subparsers)
240
+
241
+ return parser
242
+
243
+
244
+ def _add_test_command(subparsers) -> None:
245
+ """Add test connectivity command."""
246
+ test_parser = subparsers.add_parser(
247
+ "test", help="Test connectivity to D365 F&O environment"
248
+ )
249
+ test_parser.add_argument(
250
+ "--odata-only", action="store_true", help="Test only OData API connectivity"
251
+ )
252
+ test_parser.add_argument(
253
+ "--metadata-only",
254
+ action="store_true",
255
+ help="Test only Metadata API connectivity",
256
+ )
257
+ test_parser.add_argument(
258
+ "--timeout",
259
+ type=int,
260
+ default=30,
261
+ help="Connection timeout in seconds (default: 30)",
262
+ )
263
+
264
+
265
+ def _add_version_command(subparsers) -> None:
266
+ """Add version information command."""
267
+ version_parser = subparsers.add_parser(
268
+ "version", help="Get environment version information"
269
+ )
270
+ version_parser.add_argument(
271
+ "--application", action="store_true", help="Get application version"
272
+ )
273
+ version_parser.add_argument(
274
+ "--platform", action="store_true", help="Get platform build version"
275
+ )
276
+ version_parser.add_argument(
277
+ "--build", action="store_true", help="Get application build version"
278
+ )
279
+ version_parser.add_argument(
280
+ "--all", action="store_true", help="Get all version information"
281
+ )
282
+
283
+
284
+ def _add_metadata_commands(subparsers) -> None:
285
+ """Add metadata operation commands."""
286
+ metadata_parser = subparsers.add_parser("metadata", help="Metadata operations")
287
+ metadata_subs = metadata_parser.add_subparsers(
288
+ dest="metadata_subcommand", help="Metadata subcommands"
289
+ )
290
+
291
+ # sync subcommand
292
+ sync_parser = metadata_subs.add_parser("sync", help="Sync metadata to cache")
293
+ sync_parser.add_argument(
294
+ "--force", action="store_true", help="Force refresh of metadata cache"
295
+ )
296
+
297
+ # search subcommand
298
+ search_parser = metadata_subs.add_parser(
299
+ "search", help="Search metadata by pattern"
300
+ )
301
+ search_parser.add_argument("pattern", help="Search pattern")
302
+ search_parser.add_argument(
303
+ "--type",
304
+ choices=["entities", "actions", "all"],
305
+ default="entities",
306
+ help="Type of metadata to search (default: entities)",
307
+ )
308
+ search_parser.add_argument("--limit", type=int, help="Maximum number of results")
309
+
310
+ # info subcommand
311
+ info_parser = metadata_subs.add_parser("info", help="Get entity metadata details")
312
+ info_parser.add_argument("entity_name", help="Entity name")
313
+ info_parser.add_argument(
314
+ "--properties", action="store_true", help="Include property details"
315
+ )
316
+ info_parser.add_argument(
317
+ "--keys", action="store_true", help="Include key information"
318
+ )
319
+ info_parser.add_argument(
320
+ "--labels", action="store_true", help="Include label information"
321
+ )
322
+
323
+
324
+ def _add_entity_commands(subparsers) -> None:
325
+ """Add entity operation commands."""
326
+ entity_parser = subparsers.add_parser("entity", help="Entity operations")
327
+ entity_subs = entity_parser.add_subparsers(
328
+ dest="entity_subcommand", help="Entity subcommands"
329
+ )
330
+
331
+ # get subcommand
332
+ get_parser = entity_subs.add_parser("get", help="Get entity data")
333
+ get_parser.add_argument("entity_name", help="Entity name")
334
+ get_parser.add_argument(
335
+ "key", nargs="?", help="Entity key (optional, for single record)"
336
+ )
337
+ get_parser.add_argument("--select", help="Fields to select (comma-separated)")
338
+ get_parser.add_argument("--filter", help="OData filter expression")
339
+ get_parser.add_argument("--top", type=int, help="Maximum number of records")
340
+ get_parser.add_argument("--orderby", help="Order by fields (comma-separated)")
341
+
342
+ # create subcommand
343
+ create_parser = entity_subs.add_parser("create", help="Create entity record")
344
+ create_parser.add_argument("entity_name", help="Entity name")
345
+ create_parser.add_argument("--data", help="Entity data as JSON string")
346
+ create_parser.add_argument(
347
+ "--file", help="Path to JSON file containing entity data"
348
+ )
349
+
350
+ # update subcommand
351
+ update_parser = entity_subs.add_parser("update", help="Update entity record")
352
+ update_parser.add_argument("entity_name", help="Entity name")
353
+ update_parser.add_argument("key", help="Entity key")
354
+ update_parser.add_argument("--data", help="Entity data as JSON string")
355
+ update_parser.add_argument(
356
+ "--file", help="Path to JSON file containing entity data"
357
+ )
358
+
359
+ # delete subcommand
360
+ delete_parser = entity_subs.add_parser("delete", help="Delete entity record")
361
+ delete_parser.add_argument("entity_name", help="Entity name")
362
+ delete_parser.add_argument("key", help="Entity key")
363
+ delete_parser.add_argument(
364
+ "--confirm", action="store_true", help="Skip confirmation prompt"
365
+ )
366
+
367
+
368
+ def _add_action_commands(subparsers) -> None:
369
+ """Add action operation commands."""
370
+ action_parser = subparsers.add_parser("action", help="Action operations")
371
+ action_subs = action_parser.add_subparsers(
372
+ dest="action_subcommand", help="Action subcommands"
373
+ )
374
+
375
+ # list subcommand
376
+ list_parser = action_subs.add_parser("list", help="List available actions")
377
+ list_parser.add_argument(
378
+ "pattern", nargs="?", default="", help="Search pattern (optional)"
379
+ )
380
+ list_parser.add_argument("--entity", help="Filter actions for specific entity")
381
+
382
+ # call subcommand
383
+ call_parser = action_subs.add_parser("call", help="Call OData action")
384
+ call_parser.add_argument("action_name", help="Action name")
385
+ call_parser.add_argument(
386
+ "--entity", help="Entity name (if action is entity-specific)"
387
+ )
388
+ call_parser.add_argument("--parameters", help="Action parameters as JSON string")
389
+
390
+
391
+ def _add_config_commands(subparsers) -> None:
392
+ """Add configuration management commands."""
393
+ config_parser = subparsers.add_parser(
394
+ "config", help="Manage configuration profiles"
395
+ )
396
+ config_subs = config_parser.add_subparsers(
397
+ dest="config_subcommand", help="Configuration subcommands"
398
+ )
399
+
400
+ # list subcommand
401
+ config_subs.add_parser("list", help="List all configuration profiles")
402
+
403
+ # show subcommand
404
+ show_parser = config_subs.add_parser("show", help="Show profile configuration")
405
+ show_parser.add_argument("profile_name", help="Profile name")
406
+
407
+ # create subcommand
408
+ create_parser = config_subs.add_parser("create", help="Create new profile")
409
+ create_parser.add_argument("profile_name", help="Profile name")
410
+ create_parser.add_argument(
411
+ "--base-url", required=True, help="D365 F&O environment URL"
412
+ )
413
+ create_parser.add_argument(
414
+ "--auth-mode",
415
+ choices=["default", "explicit", "interactive"],
416
+ default="default",
417
+ help="Authentication mode",
418
+ )
419
+ create_parser.add_argument("--client-id", help="Azure AD client ID")
420
+ create_parser.add_argument("--client-secret", help="Azure AD client secret")
421
+ create_parser.add_argument("--tenant-id", help="Azure AD tenant ID")
422
+ create_parser.add_argument(
423
+ "--verify-ssl", type=bool, default=True, help="SSL verification"
424
+ )
425
+ create_parser.add_argument(
426
+ "--output-format",
427
+ choices=["json", "table", "csv", "yaml"],
428
+ default="table",
429
+ help="Default output format",
430
+ )
431
+ create_parser.add_argument(
432
+ "--label-cache", type=bool, default=True, help="Enable label caching"
433
+ )
434
+ create_parser.add_argument(
435
+ "--label-expiry", type=int, default=60, help="Label cache expiry in minutes"
436
+ )
437
+ create_parser.add_argument("--language", default="en-US", help="Language code")
438
+
439
+ # update subcommand (placeholder)
440
+ update_parser = config_subs.add_parser("update", help="Update existing profile")
441
+ update_parser.add_argument("profile_name", help="Profile name")
442
+
443
+ # delete subcommand
444
+ delete_parser = config_subs.add_parser("delete", help="Delete profile")
445
+ delete_parser.add_argument("profile_name", help="Profile name")
446
+
447
+ # set-default subcommand
448
+ default_parser = config_subs.add_parser("set-default", help="Set default profile")
449
+ default_parser.add_argument("profile_name", help="Profile name")
450
+
451
+
452
+ def main() -> None:
453
+ """Enhanced main entry point with CLI support."""
454
+ parser = create_argument_parser()
455
+ args = parser.parse_args()
456
+
457
+ # Handle legacy demo mode or no arguments
458
+ if (
459
+ args.demo
460
+ or (not hasattr(args, "command") or not args.command)
461
+ and len(sys.argv) == 1
462
+ ):
463
+ # Run demo if --demo specified or no arguments provided
464
+ print("Microsoft Dynamics 365 Finance & Operations Client")
465
+ print("=" * 50)
466
+
467
+ try:
468
+ asyncio.run(example_usage())
469
+ except KeyboardInterrupt:
470
+ print("\n\nOperation cancelled by user")
471
+ except Exception as e:
472
+ print(f"\n\nError: {e}")
473
+ return
474
+
475
+ # If no command specified but other arguments provided, show help
476
+ if not hasattr(args, "command") or not args.command:
477
+ parser.print_help()
478
+ return
479
+
480
+ # Create and run CLI manager
481
+ from .cli import CLIManager
482
+
483
+ cli_manager = CLIManager()
484
+
485
+ try:
486
+ exit_code = asyncio.run(cli_manager.execute_command(args))
487
+ sys.exit(exit_code)
488
+ except KeyboardInterrupt:
489
+ print("\n\nOperation cancelled by user")
490
+ sys.exit(130)
491
+ except Exception as e:
492
+ if getattr(args, "verbose", False):
493
+ import traceback
494
+
495
+ traceback.print_exc()
496
+ else:
497
+ print(f"Unexpected error: {e}")
498
+ sys.exit(1)
499
+
500
+
501
+ if __name__ == "__main__":
502
+ main()
@@ -0,0 +1,16 @@
1
+ """MCP Server for d365fo-client.
2
+
3
+ This module provides a Model Context Protocol (MCP) server that exposes the full
4
+ capabilities of the d365fo-client to AI assistants and other MCP-compatible tools.
5
+
6
+ The server enables sophisticated Microsoft Dynamics 365 Finance & Operations
7
+ integration workflows through standardized MCP protocol.
8
+ """
9
+
10
+ from .client_manager import D365FOClientManager
11
+ from .server import D365FOMCPServer
12
+
13
+ __all__ = [
14
+ "D365FOMCPServer",
15
+ "D365FOClientManager",
16
+ ]