d365fo-client 0.2.3__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.
Files changed (58) hide show
  1. d365fo_client/__init__.py +7 -1
  2. d365fo_client/auth.py +9 -21
  3. d365fo_client/cli.py +25 -13
  4. d365fo_client/client.py +8 -4
  5. d365fo_client/config.py +52 -30
  6. d365fo_client/credential_sources.py +5 -0
  7. d365fo_client/main.py +1 -1
  8. d365fo_client/mcp/__init__.py +3 -1
  9. d365fo_client/mcp/auth_server/__init__.py +5 -0
  10. d365fo_client/mcp/auth_server/auth/__init__.py +30 -0
  11. d365fo_client/mcp/auth_server/auth/auth.py +372 -0
  12. d365fo_client/mcp/auth_server/auth/oauth_proxy.py +989 -0
  13. d365fo_client/mcp/auth_server/auth/providers/__init__.py +0 -0
  14. d365fo_client/mcp/auth_server/auth/providers/azure.py +325 -0
  15. d365fo_client/mcp/auth_server/auth/providers/bearer.py +25 -0
  16. d365fo_client/mcp/auth_server/auth/providers/jwt.py +547 -0
  17. d365fo_client/mcp/auth_server/auth/redirect_validation.py +65 -0
  18. d365fo_client/mcp/auth_server/dependencies.py +136 -0
  19. d365fo_client/mcp/client_manager.py +16 -67
  20. d365fo_client/mcp/fastmcp_main.py +358 -0
  21. d365fo_client/mcp/fastmcp_server.py +598 -0
  22. d365fo_client/mcp/fastmcp_utils.py +431 -0
  23. d365fo_client/mcp/main.py +40 -13
  24. d365fo_client/mcp/mixins/__init__.py +24 -0
  25. d365fo_client/mcp/mixins/base_tools_mixin.py +55 -0
  26. d365fo_client/mcp/mixins/connection_tools_mixin.py +50 -0
  27. d365fo_client/mcp/mixins/crud_tools_mixin.py +311 -0
  28. d365fo_client/mcp/mixins/database_tools_mixin.py +685 -0
  29. d365fo_client/mcp/mixins/label_tools_mixin.py +87 -0
  30. d365fo_client/mcp/mixins/metadata_tools_mixin.py +565 -0
  31. d365fo_client/mcp/mixins/performance_tools_mixin.py +109 -0
  32. d365fo_client/mcp/mixins/profile_tools_mixin.py +713 -0
  33. d365fo_client/mcp/mixins/sync_tools_mixin.py +321 -0
  34. d365fo_client/mcp/prompts/action_execution.py +1 -1
  35. d365fo_client/mcp/prompts/sequence_analysis.py +1 -1
  36. d365fo_client/mcp/tools/crud_tools.py +3 -3
  37. d365fo_client/mcp/tools/sync_tools.py +1 -1
  38. d365fo_client/mcp/utilities/__init__.py +1 -0
  39. d365fo_client/mcp/utilities/auth.py +34 -0
  40. d365fo_client/mcp/utilities/logging.py +58 -0
  41. d365fo_client/mcp/utilities/types.py +426 -0
  42. d365fo_client/metadata_v2/sync_manager_v2.py +2 -0
  43. d365fo_client/metadata_v2/sync_session_manager.py +7 -7
  44. d365fo_client/models.py +139 -139
  45. d365fo_client/output.py +2 -2
  46. d365fo_client/profile_manager.py +62 -27
  47. d365fo_client/profiles.py +118 -113
  48. d365fo_client/settings.py +355 -0
  49. d365fo_client/sync_models.py +85 -2
  50. d365fo_client/utils.py +2 -1
  51. {d365fo_client-0.2.3.dist-info → d365fo_client-0.3.0.dist-info}/METADATA +1261 -810
  52. d365fo_client-0.3.0.dist-info/RECORD +84 -0
  53. d365fo_client-0.3.0.dist-info/entry_points.txt +4 -0
  54. d365fo_client-0.2.3.dist-info/RECORD +0 -56
  55. d365fo_client-0.2.3.dist-info/entry_points.txt +0 -3
  56. {d365fo_client-0.2.3.dist-info → d365fo_client-0.3.0.dist-info}/WHEEL +0 -0
  57. {d365fo_client-0.2.3.dist-info → d365fo_client-0.3.0.dist-info}/licenses/LICENSE +0 -0
  58. {d365fo_client-0.2.3.dist-info → d365fo_client-0.3.0.dist-info}/top_level.txt +0 -0
@@ -4,9 +4,20 @@ import uuid
4
4
  from datetime import datetime, timezone
5
5
  from dataclasses import dataclass, field
6
6
  from enum import StrEnum
7
- from typing import Dict, List, Optional, Callable, Set
7
+ from typing import Dict, List, Optional, Set
8
8
 
9
- from .models import SyncStrategy, SyncResult
9
+ # Removed import to avoid circular dependency - models will be defined here
10
+
11
+
12
+ class SyncStrategy(StrEnum):
13
+ """Metadata synchronization strategies"""
14
+
15
+ FULL = "full"
16
+ INCREMENTAL = "incremental"
17
+ ENTITIES_ONLY = "entities_only"
18
+ LABELS_ONLY = "labels_only"
19
+ SHARING_MODE = "sharing_mode"
20
+ FULL_WITHOUT_LABELS = "full_without_labels"
10
21
 
11
22
 
12
23
  class SyncStatus(StrEnum):
@@ -33,6 +44,78 @@ class SyncPhase(StrEnum):
33
44
  FAILED = "failed"
34
45
 
35
46
 
47
+ @dataclass
48
+ class SyncResult:
49
+ """Enhanced synchronization result with sharing metrics"""
50
+
51
+ sync_type: str # full|incremental|linked|skipped|failed
52
+ entities_synced: int = 0
53
+ actions_synced: int = 0
54
+ enumerations_synced: int = 0
55
+ labels_synced: int = 0
56
+ duration_ms: float = 0.0
57
+ success: bool = True
58
+ errors: List[str] = field(default_factory=list)
59
+
60
+ # Enhanced metrics for V2
61
+ entities_shared: int = 0
62
+ actions_shared: int = 0
63
+ enumerations_shared: int = 0
64
+ labels_shared: int = 0
65
+ cache_hit_rate: float = 0.0
66
+ sharing_efficiency: float = 0.0 # Percentage of items shared vs downloaded
67
+ source_version_id: Optional[int] = None # Version we shared from
68
+
69
+ def to_dict(self) -> dict:
70
+ """Convert to dictionary for JSON serialization"""
71
+ return {
72
+ "sync_type": self.sync_type,
73
+ "entities_synced": self.entities_synced,
74
+ "actions_synced": self.actions_synced,
75
+ "enumerations_synced": self.enumerations_synced,
76
+ "labels_synced": self.labels_synced,
77
+ "duration_ms": self.duration_ms,
78
+ "success": self.success,
79
+ "errors": self.errors,
80
+ "entities_shared": self.entities_shared,
81
+ "actions_shared": self.actions_shared,
82
+ "enumerations_shared": self.enumerations_shared,
83
+ "labels_shared": self.labels_shared,
84
+ "cache_hit_rate": self.cache_hit_rate,
85
+ "sharing_efficiency": self.sharing_efficiency,
86
+ "source_version_id": self.source_version_id
87
+ }
88
+
89
+
90
+ @dataclass
91
+ class SyncProgress:
92
+ """Sync progress tracking"""
93
+
94
+ global_version_id: int
95
+ strategy: SyncStrategy
96
+ phase: str
97
+ total_steps: int
98
+ completed_steps: int
99
+ current_operation: str
100
+ start_time: datetime
101
+ estimated_completion: Optional[datetime] = None
102
+ error: Optional[str] = None
103
+
104
+ def to_dict(self) -> dict:
105
+ """Convert to dictionary for JSON serialization"""
106
+ return {
107
+ "global_version_id": self.global_version_id,
108
+ "strategy": self.strategy,
109
+ "phase": self.phase,
110
+ "total_steps": self.total_steps,
111
+ "completed_steps": self.completed_steps,
112
+ "current_operation": self.current_operation,
113
+ "start_time": self.start_time.isoformat() if self.start_time else None,
114
+ "estimated_completion": self.estimated_completion.isoformat() if self.estimated_completion else None,
115
+ "error": self.error
116
+ }
117
+
118
+
36
119
  @dataclass
37
120
  class SyncActivity:
38
121
  """Individual sync activity within a phase"""
d365fo_client/utils.py CHANGED
@@ -38,7 +38,8 @@ def get_user_cache_dir(app_name: str = "d365fo-client") -> Path:
38
38
  # Falls back to APPDATA if LOCALAPPDATA is not available
39
39
  cache_root = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA")
40
40
  if cache_root:
41
- return Path(cache_root) / app_name
41
+ # Normalize path separators for consistency
42
+ return Path(cache_root.replace("\\", "/")) / app_name
42
43
  else:
43
44
  # Fallback: use user home directory
44
45
  return Path.home() / "AppData" / "Local" / app_name