d365fo-client 0.1.0__py3-none-any.whl → 0.2.1__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.
@@ -8,19 +8,25 @@ from d365fo_client.crud import CrudOperations
8
8
 
9
9
  from .labels import LabelOperations
10
10
  from .models import (
11
+ ActionInfo,
11
12
  ActionParameterInfo,
12
13
  ActionParameterTypeInfo,
13
14
  ActionReturnTypeInfo,
15
+ Cardinality,
14
16
  DataEntityInfo,
17
+ EntityCategory,
15
18
  EnumerationInfo,
16
19
  EnumerationMemberInfo,
20
+ FixedConstraintInfo,
17
21
  NavigationPropertyInfo,
22
+ ODataBindingKind,
18
23
  PropertyGroupInfo,
19
24
  PublicEntityActionInfo,
20
25
  PublicEntityInfo,
21
26
  PublicEntityPropertyInfo,
22
27
  QueryOptions,
23
28
  ReferentialConstraintInfo,
29
+ RelatedFixedConstraintInfo,
24
30
  )
25
31
  from .query import QueryBuilder
26
32
  from .session import SessionManager
@@ -97,26 +103,57 @@ class MetadataAPIOperations:
97
103
 
98
104
  # Process navigation properties
99
105
  for nav_data in item.get("NavigationProperties", []):
106
+ # Convert cardinality string to enum
107
+ cardinality_str = nav_data.get("Cardinality", "Single")
108
+ try:
109
+ if cardinality_str == "Single":
110
+ cardinality = Cardinality.SINGLE
111
+ elif cardinality_str == "Multiple":
112
+ cardinality = Cardinality.MULTIPLE
113
+ else:
114
+ cardinality = Cardinality.SINGLE # Default
115
+ except Exception:
116
+ cardinality = Cardinality.SINGLE
117
+
100
118
  nav_prop = NavigationPropertyInfo(
101
119
  name=nav_data.get("Name", ""),
102
120
  related_entity=nav_data.get("RelatedEntity", ""),
103
121
  related_relation_name=nav_data.get("RelatedRelationName"),
104
- cardinality=nav_data.get("Cardinality", "Single"),
122
+ cardinality=cardinality,
105
123
  )
106
124
 
107
125
  # Process constraints
108
126
  for constraint_data in nav_data.get("Constraints", []):
109
- # Check for ReferentialConstraint type (most common)
110
127
  odata_type = constraint_data.get("@odata.type", "")
128
+
111
129
  if "ReferentialConstraint" in odata_type:
130
+ # Referential constraint (foreign key relationship)
112
131
  constraint = ReferentialConstraintInfo(
113
- constraint_type="Referential",
114
132
  property=constraint_data.get("Property", ""),
115
133
  referenced_property=constraint_data.get(
116
134
  "ReferencedProperty", ""
117
135
  ),
118
136
  )
119
137
  nav_prop.constraints.append(constraint)
138
+
139
+ elif "RelatedFixedConstraint" in odata_type:
140
+ # Related fixed constraint (check this before FixedConstraint)
141
+ constraint = RelatedFixedConstraintInfo(
142
+ related_property=constraint_data.get("RelatedProperty", ""),
143
+ value=constraint_data.get("Value"),
144
+ value_str=constraint_data.get("ValueStr", constraint_data.get("StringValue")),
145
+ )
146
+ nav_prop.constraints.append(constraint)
147
+
148
+ elif "FixedConstraint" in odata_type:
149
+ # Fixed value constraint
150
+ constraint = FixedConstraintInfo(
151
+ property=constraint_data.get("Property", ""),
152
+ value=constraint_data.get("Value"),
153
+ value_str=constraint_data.get("ValueStr", constraint_data.get("StringValue")),
154
+ )
155
+ nav_prop.constraints.append(constraint)
156
+ nav_prop.constraints.append(constraint)
120
157
 
121
158
  entity.navigation_properties.append(nav_prop)
122
159
 
@@ -130,9 +167,25 @@ class MetadataAPIOperations:
130
167
 
131
168
  # Process actions
132
169
  for action_data in item.get("Actions", []):
170
+ # Convert binding kind string to enum
171
+ binding_kind_str = action_data.get("BindingKind", "BoundToEntitySet")
172
+ try:
173
+ # Try to map the string to the enum
174
+ if binding_kind_str == "BoundToEntityInstance":
175
+ binding_kind = ODataBindingKind.BOUND_TO_ENTITY_INSTANCE
176
+ elif binding_kind_str == "BoundToEntitySet":
177
+ binding_kind = ODataBindingKind.BOUND_TO_ENTITY_SET
178
+ elif binding_kind_str == "Unbound":
179
+ binding_kind = ODataBindingKind.UNBOUND
180
+ else:
181
+ # Default to BoundToEntitySet if unknown
182
+ binding_kind = ODataBindingKind.BOUND_TO_ENTITY_SET
183
+ except Exception:
184
+ binding_kind = ODataBindingKind.BOUND_TO_ENTITY_SET
185
+
133
186
  action = PublicEntityActionInfo(
134
187
  name=action_data.get("Name", ""),
135
- binding_kind=action_data.get("BindingKind", ""),
188
+ binding_kind=binding_kind,
136
189
  field_lookup=action_data.get("FieldLookup"),
137
190
  )
138
191
 
@@ -266,6 +319,28 @@ class MetadataAPIOperations:
266
319
 
267
320
  entities = []
268
321
  for item in data.get("value", []):
322
+ # Convert entity category string to enum
323
+ entity_category_str = item.get("EntityCategory")
324
+ entity_category = None
325
+ if entity_category_str:
326
+ try:
327
+ # Try to map the string to the enum
328
+ if entity_category_str == "Master":
329
+ entity_category = EntityCategory.MASTER
330
+ elif entity_category_str == "Transaction":
331
+ entity_category = EntityCategory.TRANSACTION
332
+ elif entity_category_str == "Configuration":
333
+ entity_category = EntityCategory.CONFIGURATION
334
+ elif entity_category_str == "Reference":
335
+ entity_category = EntityCategory.REFERENCE
336
+ elif entity_category_str == "Document":
337
+ entity_category = EntityCategory.DOCUMENT
338
+ elif entity_category_str == "Parameters":
339
+ entity_category = EntityCategory.PARAMETERS
340
+ # If no match, leave as None
341
+ except Exception:
342
+ entity_category = None
343
+
269
344
  entity = DataEntityInfo(
270
345
  name=item.get("Name", ""),
271
346
  public_entity_name=item.get("PublicEntityName", ""),
@@ -273,7 +348,7 @@ class MetadataAPIOperations:
273
348
  label_id=item.get("LabelId"),
274
349
  data_service_enabled=item.get("DataServiceEnabled", False),
275
350
  data_management_enabled=item.get("DataManagementEnabled", False),
276
- entity_category=item.get("EntityCategory"),
351
+ entity_category=entity_category,
277
352
  is_read_only=item.get("IsReadOnly", False),
278
353
  )
279
354
  entities.append(entity)
@@ -305,6 +380,29 @@ class MetadataAPIOperations:
305
380
  async with session.get(url) as response:
306
381
  if response.status == 200:
307
382
  item = await response.json()
383
+
384
+ # Convert entity category string to enum
385
+ entity_category_str = item.get("EntityCategory")
386
+ entity_category = None
387
+ if entity_category_str:
388
+ try:
389
+ # Try to map the string to the enum
390
+ if entity_category_str == "Master":
391
+ entity_category = EntityCategory.MASTER
392
+ elif entity_category_str == "Transaction":
393
+ entity_category = EntityCategory.TRANSACTION
394
+ elif entity_category_str == "Configuration":
395
+ entity_category = EntityCategory.CONFIGURATION
396
+ elif entity_category_str == "Reference":
397
+ entity_category = EntityCategory.REFERENCE
398
+ elif entity_category_str == "Document":
399
+ entity_category = EntityCategory.DOCUMENT
400
+ elif entity_category_str == "Parameters":
401
+ entity_category = EntityCategory.PARAMETERS
402
+ # If no match, leave as None
403
+ except Exception:
404
+ entity_category = None
405
+
308
406
  entity = DataEntityInfo(
309
407
  name=item.get("Name", ""),
310
408
  public_entity_name=item.get("PublicEntityName", ""),
@@ -314,7 +412,7 @@ class MetadataAPIOperations:
314
412
  data_management_enabled=item.get(
315
413
  "DataManagementEnabled", False
316
414
  ),
317
- entity_category=item.get("EntityCategory"),
415
+ entity_category=entity_category,
318
416
  is_read_only=item.get("IsReadOnly", False),
319
417
  )
320
418
 
@@ -791,3 +889,122 @@ class MetadataAPIOperations:
791
889
  except Exception as e:
792
890
  logger.error(f"Failed to get installed modules: {e}")
793
891
  raise
892
+
893
+ # Action Operations
894
+
895
+ async def search_actions(
896
+ self,
897
+ pattern: str = "",
898
+ entity_name: Optional[str] = None,
899
+ binding_kind: Optional[str] = None,
900
+ ) -> List["ActionInfo"]:
901
+ """Search actions across all public entities
902
+
903
+ Args:
904
+ pattern: Search pattern for action name (regex supported)
905
+ entity_name: Filter actions that are bound to a specific entity
906
+ binding_kind: Filter by binding type (Unbound, BoundToEntitySet, BoundToEntityInstance)
907
+
908
+ Returns:
909
+ List of ActionInfo objects with full details
910
+ """
911
+ from .models import ActionInfo
912
+
913
+ actions = []
914
+
915
+ try:
916
+ # Compile regex pattern if provided
917
+ regex_pattern = None
918
+ if pattern:
919
+ try:
920
+ regex_pattern = re.compile(pattern, re.IGNORECASE)
921
+ except re.error:
922
+ # If regex is invalid, treat as literal string
923
+ pattern_lower = pattern.lower()
924
+ regex_pattern = lambda x: pattern_lower in x.lower()
925
+ else:
926
+ regex_pattern = regex_pattern.search
927
+
928
+ # Get all public entities with full details
929
+ entities = await self.get_all_public_entities_with_details(resolve_labels=False)
930
+
931
+ for entity in entities:
932
+ # Filter by entity name if specified
933
+ if entity_name and entity.name != entity_name:
934
+ continue
935
+
936
+ # Process all actions in this entity
937
+ for action in entity.actions:
938
+ # Filter by action name pattern
939
+ if regex_pattern and not regex_pattern(action.name):
940
+ continue
941
+
942
+ # Filter by binding kind
943
+ if binding_kind and action.binding_kind.value != binding_kind:
944
+ continue
945
+
946
+ # Create ActionInfo with entity context
947
+ action_info = ActionInfo(
948
+ name=action.name,
949
+ binding_kind=action.binding_kind,
950
+ entity_name=entity.name, # public entity name
951
+ entity_set_name=entity.entity_set_name, # entity set name for OData URLs
952
+ parameters=action.parameters,
953
+ return_type=action.return_type,
954
+ field_lookup=action.field_lookup,
955
+ )
956
+ actions.append(action_info)
957
+
958
+ except Exception as e:
959
+ logger.error(f"Failed to search actions: {e}")
960
+ # Return empty list on error rather than raising
961
+ return []
962
+
963
+ return actions
964
+
965
+ async def get_action_info(
966
+ self,
967
+ action_name: str,
968
+ entity_name: Optional[str] = None,
969
+ ) -> Optional["ActionInfo"]:
970
+ """Get detailed information about a specific action
971
+
972
+ Args:
973
+ action_name: Name of the action
974
+ entity_name: Optional entity name for bound actions
975
+
976
+ Returns:
977
+ ActionInfo object or None if not found
978
+ """
979
+ from .models import ActionInfo
980
+
981
+ try:
982
+ if entity_name:
983
+ # If entity name is provided, get that specific entity
984
+ entity = await self.get_public_entity_info(entity_name, resolve_labels=False)
985
+ if not entity:
986
+ return None
987
+
988
+ # Find the action in this entity
989
+ for action in entity.actions:
990
+ if action.name == action_name:
991
+ return ActionInfo(
992
+ name=action.name,
993
+ binding_kind=action.binding_kind,
994
+ entity_name=entity.name,
995
+ entity_set_name=entity.entity_set_name,
996
+ parameters=action.parameters,
997
+ return_type=action.return_type,
998
+ field_lookup=action.field_lookup,
999
+ )
1000
+ else:
1001
+ # Search across all entities for the action
1002
+ actions = await self.search_actions(pattern=f"^{re.escape(action_name)}$")
1003
+ if actions:
1004
+ # Return the first match (actions should be unique across entities)
1005
+ return actions[0]
1006
+
1007
+ except Exception as e:
1008
+ logger.error(f"Failed to get action info for '{action_name}': {e}")
1009
+
1010
+ return None