d365fo-client 0.3.0__py3-none-any.whl → 0.3.2__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/cli.py CHANGED
@@ -99,6 +99,7 @@ class CLIManager:
99
99
  "metadata": self._handle_metadata_commands,
100
100
  "entity": self._handle_entity_commands,
101
101
  "action": self._handle_action_commands,
102
+ "service": self._handle_service_commands,
102
103
  }
103
104
 
104
105
  command = getattr(args, "command", None)
@@ -694,6 +695,140 @@ class CLIManager:
694
695
  print(format_error_message(f"Error setting default profile: {e}"))
695
696
  return 1
696
697
 
698
+ async def _handle_service_commands(self, args: argparse.Namespace) -> int:
699
+ """Handle JSON service commands."""
700
+ subcommand = getattr(args, "service_subcommand", None)
701
+
702
+ if subcommand == "call":
703
+ return await self._handle_service_call(args)
704
+ elif subcommand == "sql-diagnostic":
705
+ return await self._handle_service_sql_diagnostic(args)
706
+ else:
707
+ print(format_error_message(f"Unknown service subcommand: {subcommand}"))
708
+ return 1
709
+
710
+ async def _handle_service_call(self, args: argparse.Namespace) -> int:
711
+ """Handle generic JSON service call command."""
712
+ try:
713
+ service_group = getattr(args, "service_group", "")
714
+ service_name = getattr(args, "service_name", "")
715
+ operation_name = getattr(args, "operation_name", "")
716
+
717
+ # Parse parameters from JSON string if provided
718
+ parameters = None
719
+ parameters_str = getattr(args, "parameters", None)
720
+ if parameters_str:
721
+ try:
722
+ parameters = json.loads(parameters_str)
723
+ except json.JSONDecodeError as e:
724
+ print(format_error_message(f"Invalid JSON in parameters: {e}"))
725
+ return 1
726
+
727
+ # Call the service
728
+ response = await self.client.post_json_service(
729
+ service_group=service_group,
730
+ service_name=service_name,
731
+ operation_name=operation_name,
732
+ parameters=parameters,
733
+ )
734
+
735
+ # Format and display response
736
+ if response.success:
737
+ result = {
738
+ "success": True,
739
+ "statusCode": response.status_code,
740
+ "data": response.data,
741
+ "serviceGroup": service_group,
742
+ "serviceName": service_name,
743
+ "operationName": operation_name,
744
+ }
745
+ output = self.output_formatter.format_output(result)
746
+ print(output)
747
+ return 0
748
+ else:
749
+ error_result = {
750
+ "success": False,
751
+ "statusCode": response.status_code,
752
+ "error": response.error_message,
753
+ "serviceGroup": service_group,
754
+ "serviceName": service_name,
755
+ "operationName": operation_name,
756
+ }
757
+ output = self.output_formatter.format_output(error_result)
758
+ print(output)
759
+ return 1
760
+
761
+ except Exception as e:
762
+ print(format_error_message(f"Error calling service: {e}"))
763
+ return 1
764
+
765
+ async def _handle_service_sql_diagnostic(self, args: argparse.Namespace) -> int:
766
+ """Handle SQL diagnostic service call command."""
767
+ try:
768
+ operation = getattr(args, "operation", "")
769
+
770
+ # Prepare parameters based on operation
771
+ parameters = {}
772
+
773
+ if operation == "GetAxSqlResourceStats":
774
+ since_minutes = getattr(args, "since_minutes", 10)
775
+ start_time = getattr(args, "start_time", None)
776
+ end_time = getattr(args, "end_time", None)
777
+
778
+ if start_time and end_time:
779
+ parameters = {
780
+ "start": start_time,
781
+ "end": end_time,
782
+ }
783
+ else:
784
+ # Use since_minutes to calculate start/end
785
+ from datetime import datetime, timezone, timedelta
786
+ end = datetime.now(timezone.utc)
787
+ start = end - timedelta(minutes=since_minutes)
788
+ parameters = {
789
+ "start": start.isoformat(),
790
+ "end": end.isoformat(),
791
+ }
792
+
793
+ # Call the SQL diagnostic service
794
+ response = await self.client.post_json_service(
795
+ service_group="SysSqlDiagnosticService",
796
+ service_name="SysSqlDiagnosticServiceOperations",
797
+ operation_name=operation,
798
+ parameters=parameters if parameters else None,
799
+ )
800
+
801
+ # Format and display response
802
+ if response.success:
803
+ result = {
804
+ "success": True,
805
+ "statusCode": response.status_code,
806
+ "operation": operation,
807
+ "data": response.data,
808
+ }
809
+
810
+ # Add summary information
811
+ if isinstance(response.data, list):
812
+ result["recordCount"] = len(response.data)
813
+
814
+ output = self.output_formatter.format_output(result)
815
+ print(output)
816
+ return 0
817
+ else:
818
+ error_result = {
819
+ "success": False,
820
+ "statusCode": response.status_code,
821
+ "operation": operation,
822
+ "error": response.error_message,
823
+ }
824
+ output = self.output_formatter.format_output(error_result)
825
+ print(output)
826
+ return 1
827
+
828
+ except Exception as e:
829
+ print(format_error_message(f"Error calling SQL diagnostic service: {e}"))
830
+ return 1
831
+
697
832
  def _handle_error(self, error: Exception, verbose: bool = False) -> None:
698
833
  """Handle and display errors consistently.
699
834
 
d365fo_client/client.py CHANGED
@@ -19,6 +19,8 @@ from .models import (
19
19
  DataEntityInfo,
20
20
  EnumerationInfo,
21
21
  FOClientConfig,
22
+ JsonServiceRequest,
23
+ JsonServiceResponse,
22
24
  PublicEntityInfo,
23
25
  QueryOptions,
24
26
  )
@@ -531,36 +533,58 @@ class FOClient:
531
533
  # CRUD Operations
532
534
 
533
535
  async def get_entities(
534
- self, entity_name: str, options: Optional[QueryOptions] = None
536
+ self, entity_name: str, options: Optional[QueryOptions] = None, skip_validation: bool = False
535
537
  ) -> Dict[str, Any]:
536
538
  """Get entities with OData query options
537
539
 
538
540
  Args:
539
541
  entity_name: Name of the entity set
540
542
  options: OData query options
543
+ skip_validation: Skip schema validation for performance
541
544
 
542
545
  Returns:
543
546
  Response containing entities
544
547
  """
545
- return await self.crud_ops.get_entities(entity_name, options)
548
+ entity_schema = None
549
+ if not skip_validation:
550
+ entity_schema = await self.get_public_entity_schema_by_entityset(entity_name)
551
+ if not entity_schema:
552
+ raise FOClientError(
553
+ f"Entity '{entity_name}' not found or not accessible for OData operations"
554
+ )
555
+
556
+ return await self.crud_ops.get_entities(entity_name, options, entity_schema)
546
557
 
547
558
  async def get_entity(
548
559
  self,
549
560
  entity_name: str,
550
561
  key: Union[str, Dict[str, Any]],
551
562
  options: Optional[QueryOptions] = None,
563
+ skip_validation: bool = False,
552
564
  ) -> Dict[str, Any]:
553
- """Get single entity by key
565
+ """Get single entity by key with schema validation
554
566
 
555
567
  Args:
556
- entity_name: Name of the entity set
568
+ entity_name: Name of the entity set (entityset/collection name)
557
569
  key: Entity key value (string for simple keys, dict for composite keys)
558
570
  options: OData query options
571
+ skip_validation: Skip schema validation for performance (batch operations)
559
572
 
560
573
  Returns:
561
574
  Entity data
575
+
576
+ Raises:
577
+ FOClientError: If entity not found or not accessible
562
578
  """
563
- return await self.crud_ops.get_entity(entity_name, key, options)
579
+ entity_schema = None
580
+ if not skip_validation:
581
+ entity_schema = await self.get_public_entity_schema_by_entityset(entity_name)
582
+ if not entity_schema:
583
+ raise FOClientError(
584
+ f"Entity '{entity_name}' not found or not accessible for OData operations"
585
+ )
586
+
587
+ return await self.crud_ops.get_entity(entity_name, key, options, entity_schema)
564
588
 
565
589
  async def get_entity_by_key(
566
590
  self,
@@ -568,6 +592,7 @@ class FOClient:
568
592
  key: Union[str, Dict[str, Any]],
569
593
  select: Optional[List[str]] = None,
570
594
  expand: Optional[List[str]] = None,
595
+ skip_validation: bool = False,
571
596
  ) -> Optional[Dict[str, Any]]:
572
597
  """Get single entity by key with optional field selection and expansion
573
598
 
@@ -576,6 +601,7 @@ class FOClient:
576
601
  key: Entity key value (string for simple keys, dict for composite keys)
577
602
  select: Optional list of fields to select
578
603
  expand: Optional list of navigation properties to expand
604
+ skip_validation: Skip schema validation for performance
579
605
 
580
606
  Returns:
581
607
  Entity data or None if not found
@@ -584,7 +610,7 @@ class FOClient:
584
610
  options = (
585
611
  QueryOptions(select=select, expand=expand) if select or expand else None
586
612
  )
587
- return await self.crud_ops.get_entity(entity_name, key, options)
613
+ return await self.get_entity(entity_name, key, options, skip_validation)
588
614
  except Exception as e:
589
615
  # If the entity is not found, return None instead of raising exception
590
616
  if "404" in str(e):
@@ -592,18 +618,34 @@ class FOClient:
592
618
  raise
593
619
 
594
620
  async def create_entity(
595
- self, entity_name: str, data: Dict[str, Any]
621
+ self, entity_name: str, data: Dict[str, Any], skip_validation: bool = False
596
622
  ) -> Dict[str, Any]:
597
- """Create new entity
623
+ """Create new entity with schema validation
598
624
 
599
625
  Args:
600
626
  entity_name: Name of the entity set
601
627
  data: Entity data to create
628
+ skip_validation: Skip schema validation for performance (batch operations)
602
629
 
603
630
  Returns:
604
631
  Created entity data
632
+
633
+ Raises:
634
+ FOClientError: If entity not found, read-only, or validation fails
605
635
  """
606
- return await self.crud_ops.create_entity(entity_name, data)
636
+ entity_schema = None
637
+ if not skip_validation:
638
+ entity_schema = await self.get_public_entity_schema_by_entityset(entity_name)
639
+ if not entity_schema:
640
+ raise FOClientError(
641
+ f"Entity '{entity_name}' not found or not accessible for OData operations"
642
+ )
643
+ if entity_schema.is_read_only:
644
+ raise FOClientError(
645
+ f"Entity '{entity_name}' is read-only and cannot accept create operations"
646
+ )
647
+
648
+ return await self.crud_ops.create_entity(entity_name, data, entity_schema)
607
649
 
608
650
  async def update_entity(
609
651
  self,
@@ -611,33 +653,66 @@ class FOClient:
611
653
  key: Union[str, Dict[str, Any]],
612
654
  data: Dict[str, Any],
613
655
  method: str = "PATCH",
656
+ skip_validation: bool = False,
614
657
  ) -> Dict[str, Any]:
615
- """Update existing entity
658
+ """Update existing entity with schema validation
616
659
 
617
660
  Args:
618
661
  entity_name: Name of the entity set
619
662
  key: Entity key value (string for simple keys, dict for composite keys)
620
663
  data: Updated entity data
621
664
  method: HTTP method (PATCH or PUT)
665
+ skip_validation: Skip schema validation for performance (batch operations)
622
666
 
623
667
  Returns:
624
668
  Updated entity data
669
+
670
+ Raises:
671
+ FOClientError: If entity not found, read-only, or validation fails
625
672
  """
626
- return await self.crud_ops.update_entity(entity_name, key, data, method)
673
+ entity_schema = None
674
+ if not skip_validation:
675
+ entity_schema = await self.get_public_entity_schema_by_entityset(entity_name)
676
+ if not entity_schema:
677
+ raise FOClientError(
678
+ f"Entity '{entity_name}' not found or not accessible for OData operations"
679
+ )
680
+ if entity_schema.is_read_only:
681
+ raise FOClientError(
682
+ f"Entity '{entity_name}' is read-only and cannot accept update operations"
683
+ )
684
+
685
+ return await self.crud_ops.update_entity(entity_name, key, data, method, entity_schema)
627
686
 
628
687
  async def delete_entity(
629
- self, entity_name: str, key: Union[str, Dict[str, Any]]
688
+ self, entity_name: str, key: Union[str, Dict[str, Any]], skip_validation: bool = False
630
689
  ) -> bool:
631
- """Delete entity
690
+ """Delete entity with schema validation
632
691
 
633
692
  Args:
634
693
  entity_name: Name of the entity set
635
694
  key: Entity key value (string for simple keys, dict for composite keys)
695
+ skip_validation: Skip schema validation for performance (batch operations)
636
696
 
637
697
  Returns:
638
698
  True if successful
699
+
700
+ Raises:
701
+ FOClientError: If entity not found, read-only, or validation fails
639
702
  """
640
- return await self.crud_ops.delete_entity(entity_name, key)
703
+ entity_schema = None
704
+ if not skip_validation:
705
+ entity_schema = await self.get_public_entity_schema_by_entityset(entity_name)
706
+ if not entity_schema:
707
+ raise FOClientError(
708
+ f"Entity '{entity_name}' not found or not accessible for OData operations"
709
+ )
710
+ if entity_schema.is_read_only:
711
+ raise FOClientError(
712
+ f"Entity '{entity_name}' is read-only and cannot accept delete operations"
713
+ )
714
+
715
+ return await self.crud_ops.delete_entity(entity_name, key, entity_schema)
641
716
 
642
717
  async def call_action(
643
718
  self,
@@ -645,6 +720,7 @@ class FOClient:
645
720
  parameters: Optional[Dict[str, Any]] = None,
646
721
  entity_name: Optional[str] = None,
647
722
  entity_key: Optional[Union[str, Dict[str, Any]]] = None,
723
+ skip_validation: bool = False,
648
724
  ) -> Any:
649
725
  """Call OData action method
650
726
 
@@ -653,12 +729,21 @@ class FOClient:
653
729
  parameters: Action parameters
654
730
  entity_name: Entity name for bound actions
655
731
  entity_key: Entity key for bound actions (string for simple keys, dict for composite keys)
732
+ skip_validation: Skip schema validation for performance
656
733
 
657
734
  Returns:
658
735
  Action result
659
736
  """
737
+ entity_schema = None
738
+ if not skip_validation and entity_name:
739
+ entity_schema = await self.get_public_entity_schema_by_entityset(entity_name)
740
+ if not entity_schema:
741
+ raise FOClientError(
742
+ f"Entity '{entity_name}' not found or not accessible for OData operations"
743
+ )
744
+
660
745
  return await self.crud_ops.call_action(
661
- action_name, parameters, entity_name, entity_key
746
+ action_name, parameters, entity_name, entity_key, entity_schema
662
747
  )
663
748
 
664
749
  # Label Operations
@@ -1244,17 +1329,284 @@ class FOClient:
1244
1329
 
1245
1330
  # Application Version Operations
1246
1331
 
1247
- async def get_entity_schema(self, entity_name: str) -> Optional[PublicEntityInfo]:
1248
- """Get entity schema - compatibility method for SmartSyncManagerV2
1332
+ async def post_json_service(
1333
+ self,
1334
+ service_group: str,
1335
+ service_name: str,
1336
+ operation_name: str,
1337
+ parameters: Optional[Dict[str, Any]] = None,
1338
+ ) -> JsonServiceResponse:
1339
+ """Call D365 F&O JSON service endpoint using POST method
1340
+
1341
+ This method provides a generic way to call D365 F&O JSON services that use the
1342
+ /api/services/{ServiceGroup}/{ServiceName}/{OperationName} endpoint pattern.
1343
+
1344
+ Args:
1345
+ service_group: Service group name (e.g., "SysSqlDiagnosticService")
1346
+ service_name: Service name (e.g., "SysSqlDiagnosticServiceOperations")
1347
+ operation_name: Operation name (e.g., "GetAxSqlExecuting")
1348
+ parameters: Optional parameters to send in the POST body
1349
+
1350
+ Returns:
1351
+ JsonServiceResponse containing the result data and metadata
1352
+
1353
+ Raises:
1354
+ FOClientError: If the service call fails
1355
+
1356
+ Example:
1357
+ # Call a service without parameters
1358
+ response = await client.post_json_service(
1359
+ "SysSqlDiagnosticService",
1360
+ "SysSqlDiagnosticServiceOperations",
1361
+ "GetAxSqlExecuting"
1362
+ )
1363
+
1364
+ # Call a service with parameters
1365
+ response = await client.post_json_service(
1366
+ "SysSqlDiagnosticService",
1367
+ "SysSqlDiagnosticServiceOperations",
1368
+ "GetAxSqlResourceStats",
1369
+ {
1370
+ "start": "2023-01-01T00:00:00Z",
1371
+ "end": "2023-01-02T00:00:00Z"
1372
+ }
1373
+ )
1374
+ """
1375
+ try:
1376
+ # Create service request
1377
+ request = JsonServiceRequest(
1378
+ service_group=service_group,
1379
+ service_name=service_name,
1380
+ operation_name=operation_name,
1381
+ parameters=parameters,
1382
+ )
1383
+
1384
+ # Get the endpoint path
1385
+ endpoint_path = request.get_endpoint_path()
1386
+ url = f"{self.config.base_url.rstrip('/')}{endpoint_path}"
1387
+
1388
+ # Get session and make request
1389
+ session = await self.session_manager.get_session()
1390
+
1391
+ # Prepare headers
1392
+ headers = {"Content-Type": "application/json"}
1393
+
1394
+ # Prepare request body
1395
+ body = parameters or {}
1396
+
1397
+ async with session.post(url, json=body, headers=headers) as response:
1398
+ status_code = response.status
1399
+
1400
+ # Handle success cases
1401
+ if status_code in [200, 201]:
1402
+ try:
1403
+ content_type = response.headers.get("content-type", "")
1404
+ if "application/json" in content_type:
1405
+ data = await response.json()
1406
+ else:
1407
+ data = await response.text()
1408
+
1409
+ return JsonServiceResponse(
1410
+ success=True,
1411
+ data=data,
1412
+ status_code=status_code,
1413
+ )
1414
+ except Exception as parse_error:
1415
+ # If we can't parse the response, still return success with raw text
1416
+ text_data = await response.text()
1417
+ return JsonServiceResponse(
1418
+ success=True,
1419
+ data=text_data,
1420
+ status_code=status_code,
1421
+ error_message=f"Response parsing warning: {parse_error}",
1422
+ )
1423
+
1424
+ # Handle error cases
1425
+ else:
1426
+ error_text = await response.text()
1427
+ return JsonServiceResponse(
1428
+ success=False,
1429
+ data=None,
1430
+ status_code=status_code,
1431
+ error_message=f"HTTP {status_code}: {error_text}",
1432
+ )
1433
+
1434
+ except Exception as e:
1435
+ # Handle network errors and other exceptions
1436
+ return JsonServiceResponse(
1437
+ success=False,
1438
+ data=None,
1439
+ status_code=0,
1440
+ error_message=f"Request failed: {e}",
1441
+ )
1442
+
1443
+ async def call_json_service(
1444
+ self,
1445
+ request: JsonServiceRequest,
1446
+ ) -> JsonServiceResponse:
1447
+ """Call D365 F&O JSON service using a JsonServiceRequest object
1448
+
1449
+ This is an alternative interface to post_json_service that accepts a request object.
1450
+
1451
+ Args:
1452
+ request: JsonServiceRequest containing service details and parameters
1453
+
1454
+ Returns:
1455
+ JsonServiceResponse containing the result data and metadata
1456
+
1457
+ Example:
1458
+ request = JsonServiceRequest(
1459
+ service_group="SysSqlDiagnosticService",
1460
+ service_name="SysSqlDiagnosticServiceOperations",
1461
+ operation_name="GetAxSqlExecuting"
1462
+ )
1463
+ response = await client.call_json_service(request)
1464
+ """
1465
+ return await self.post_json_service(
1466
+ request.service_group,
1467
+ request.service_name,
1468
+ request.operation_name,
1469
+ request.parameters,
1470
+ )
1471
+
1472
+ async def get_public_entity_schema_by_entityset(
1473
+ self,
1474
+ entityset_name: str,
1475
+ use_cache_first: Optional[bool] = True
1476
+ ) -> Optional[PublicEntityInfo]:
1477
+ """Get public entity schema by entityset name (public collection name).
1478
+
1479
+ This method resolves the entityset name to the actual public entity name
1480
+ and retrieves the full schema with cache-first optimization.
1481
+
1482
+ Args:
1483
+ entityset_name: Public collection name or entity set name
1484
+ (e.g., "CustomersV3", "SalesOrders", "DataManagementEntities")
1485
+ use_cache_first: Use metadata cache before F&O API (default: True)
1486
+
1487
+ Returns:
1488
+ PublicEntityInfo with full schema, or None if entity not found
1489
+
1490
+ Resolution Logic:
1491
+ 1. Try direct lookup in public entities (entityset_name == entity name)
1492
+ 2. Search data entities for public_collection_name match
1493
+ 3. Resolve to public_entity_name and fetch schema
1494
+ 4. Use cache-first pattern for all lookups
1495
+ """
1496
+
1497
+ async def cache_lookup():
1498
+ if not self.metadata_cache:
1499
+ return None
1500
+
1501
+ # Try direct public entity lookup first (entityset_name might be the entity name)
1502
+ entity_schema = await self.metadata_cache.get_public_entity_schema(entityset_name)
1503
+ if entity_schema:
1504
+ self.logger.debug(f"Found entity schema directly for '{entityset_name}'")
1505
+ return entity_schema
1506
+
1507
+ # Try resolving via data entity metadata
1508
+ # Search for entities matching the entityset name
1509
+ data_entities = await self.metadata_cache.get_data_entities(
1510
+ name_pattern=entityset_name
1511
+ )
1512
+
1513
+ for data_entity in data_entities:
1514
+ # Check if public_collection_name matches
1515
+ if data_entity.public_collection_name == entityset_name:
1516
+ self.logger.debug(
1517
+ f"Resolved '{entityset_name}' to entity '{data_entity.public_entity_name or data_entity.name}'"
1518
+ )
1519
+ # Found match - get schema by public_entity_name
1520
+ return await self.metadata_cache.get_public_entity_schema(
1521
+ data_entity.public_entity_name or data_entity.name
1522
+ )
1523
+
1524
+ # Also check if entity name matches (for direct name usage)
1525
+ if data_entity.name == entityset_name:
1526
+ self.logger.debug(f"Found entity by name '{entityset_name}'")
1527
+ return await self.metadata_cache.get_public_entity_schema(
1528
+ data_entity.public_entity_name or data_entity.name
1529
+ )
1530
+
1531
+ # Check if public_entity_name matches
1532
+ if data_entity.public_entity_name == entityset_name:
1533
+ self.logger.debug(f"Found entity by public_entity_name '{entityset_name}'")
1534
+ return await self.metadata_cache.get_public_entity_schema(entityset_name)
1535
+
1536
+ return None
1537
+
1538
+ async def fallback_lookup():
1539
+ # Try direct public entity lookup first
1540
+ try:
1541
+ entity_schema = await self.metadata_api_ops.get_public_entity_info(
1542
+ entityset_name, resolve_labels=False
1543
+ )
1544
+ if entity_schema:
1545
+ self.logger.debug(f"Found entity schema via API for '{entityset_name}'")
1546
+ return entity_schema
1547
+ except Exception as e:
1548
+ self.logger.debug(f"Direct API lookup failed for '{entityset_name}': {e}")
1549
+
1550
+ # Search data entities to find the mapping
1551
+ try:
1552
+ data_entities = await self.metadata_api_ops.search_data_entities("")
1553
+
1554
+ for data_entity in data_entities:
1555
+ # Check if public_collection_name matches
1556
+ if data_entity.public_collection_name == entityset_name:
1557
+ self.logger.debug(
1558
+ f"Resolved '{entityset_name}' to entity '{data_entity.public_entity_name or data_entity.name}' via API"
1559
+ )
1560
+ # Found match - get schema by public_entity_name
1561
+ return await self.metadata_api_ops.get_public_entity_info(
1562
+ data_entity.public_entity_name or data_entity.name,
1563
+ resolve_labels=False
1564
+ )
1565
+
1566
+ # Also check if entity name matches
1567
+ if data_entity.name == entityset_name:
1568
+ return await self.metadata_api_ops.get_public_entity_info(
1569
+ data_entity.public_entity_name or data_entity.name,
1570
+ resolve_labels=False
1571
+ )
1572
+
1573
+ # Check if public_entity_name matches
1574
+ if data_entity.public_entity_name == entityset_name:
1575
+ return await self.metadata_api_ops.get_public_entity_info(
1576
+ entityset_name,
1577
+ resolve_labels=False
1578
+ )
1579
+ except Exception as e:
1580
+ self.logger.debug(f"Data entity search failed for '{entityset_name}': {e}")
1581
+
1582
+ return None
1583
+
1584
+ return await self._get_from_cache_first(
1585
+ cache_lookup,
1586
+ fallback_lookup,
1587
+ use_cache_first=use_cache_first,
1588
+ )
1589
+
1590
+ async def get_entity_schema(
1591
+ self,
1592
+ entity_name: str,
1593
+ use_cache_first: Optional[bool] = True
1594
+ ) -> Optional[PublicEntityInfo]:
1595
+ """Get entity schema with cache-first optimization.
1596
+
1597
+ This is an enhanced version that uses the cache-first pattern for better performance.
1249
1598
 
1250
1599
  Args:
1251
1600
  entity_name: Name of the public entity
1601
+ use_cache_first: Use metadata cache before F&O API (default: True)
1252
1602
 
1253
1603
  Returns:
1254
1604
  PublicEntityInfo object with schema details or None if not found
1255
1605
  """
1256
- return await self.metadata_api_ops.get_public_entity_info(
1257
- entity_name, resolve_labels=False
1606
+ return await self.get_public_entity_info(
1607
+ entity_name,
1608
+ resolve_labels=False,
1609
+ use_cache_first=use_cache_first
1258
1610
  )
1259
1611
 
1260
1612
  async def get_application_version(self) -> str: