infrahub-server 1.1.1__py3-none-any.whl → 1.1.3__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 (137) hide show
  1. infrahub/api/__init__.py +13 -5
  2. infrahub/api/artifact.py +9 -15
  3. infrahub/api/auth.py +7 -1
  4. infrahub/api/dependencies.py +15 -2
  5. infrahub/api/diff/diff.py +13 -7
  6. infrahub/api/file.py +5 -10
  7. infrahub/api/internal.py +19 -6
  8. infrahub/api/menu.py +8 -6
  9. infrahub/api/oauth2.py +25 -10
  10. infrahub/api/oidc.py +26 -10
  11. infrahub/api/query.py +2 -2
  12. infrahub/api/schema.py +48 -59
  13. infrahub/api/storage.py +8 -8
  14. infrahub/api/transformation.py +6 -5
  15. infrahub/auth.py +1 -26
  16. infrahub/cli/__init__.py +1 -1
  17. infrahub/cli/context.py +5 -8
  18. infrahub/cli/db.py +6 -6
  19. infrahub/cli/git_agent.py +1 -1
  20. infrahub/computed_attribute/models.py +1 -1
  21. infrahub/computed_attribute/tasks.py +1 -1
  22. infrahub/config.py +5 -5
  23. infrahub/core/account.py +2 -10
  24. infrahub/core/attribute.py +22 -0
  25. infrahub/core/branch/models.py +1 -1
  26. infrahub/core/branch/tasks.py +4 -3
  27. infrahub/core/diff/calculator.py +14 -0
  28. infrahub/core/diff/combiner.py +6 -2
  29. infrahub/core/diff/conflicts_enricher.py +2 -2
  30. infrahub/core/diff/coordinator.py +296 -87
  31. infrahub/core/diff/data_check_synchronizer.py +33 -4
  32. infrahub/core/diff/enricher/cardinality_one.py +3 -3
  33. infrahub/core/diff/enricher/hierarchy.py +4 -1
  34. infrahub/core/diff/merger/merger.py +11 -1
  35. infrahub/core/diff/merger/serializer.py +5 -29
  36. infrahub/core/diff/model/path.py +88 -4
  37. infrahub/core/diff/query/field_specifiers.py +35 -0
  38. infrahub/core/diff/query/roots_metadata.py +48 -0
  39. infrahub/core/diff/query/save.py +1 -0
  40. infrahub/core/diff/query_parser.py +27 -11
  41. infrahub/core/diff/repository/deserializer.py +7 -3
  42. infrahub/core/diff/repository/repository.py +100 -9
  43. infrahub/core/diff/tasks.py +1 -1
  44. infrahub/core/graph/__init__.py +1 -1
  45. infrahub/core/integrity/object_conflict/conflict_recorder.py +6 -1
  46. infrahub/core/ipam/utilization.py +6 -1
  47. infrahub/core/manager.py +8 -0
  48. infrahub/core/merge.py +6 -1
  49. infrahub/core/migrations/graph/__init__.py +2 -0
  50. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +1 -1
  51. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -1
  52. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -1
  53. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +101 -0
  54. infrahub/core/migrations/query/attribute_add.py +5 -5
  55. infrahub/core/migrations/schema/tasks.py +2 -2
  56. infrahub/core/migrations/shared.py +3 -3
  57. infrahub/core/node/__init__.py +8 -2
  58. infrahub/core/node/constraints/grouped_uniqueness.py +9 -2
  59. infrahub/core/query/__init__.py +5 -2
  60. infrahub/core/query/diff.py +32 -19
  61. infrahub/core/query/ipam.py +30 -22
  62. infrahub/core/query/node.py +91 -40
  63. infrahub/core/schema/generated/attribute_schema.py +2 -2
  64. infrahub/core/schema/generated/base_node_schema.py +2 -2
  65. infrahub/core/schema/generated/relationship_schema.py +1 -1
  66. infrahub/core/schema/schema_branch_computed.py +1 -1
  67. infrahub/core/task/task_log.py +1 -1
  68. infrahub/core/validators/attribute/kind.py +1 -1
  69. infrahub/core/validators/interface.py +1 -2
  70. infrahub/core/validators/models/violation.py +1 -14
  71. infrahub/core/validators/shared.py +2 -2
  72. infrahub/core/validators/tasks.py +7 -4
  73. infrahub/core/validators/uniqueness/index.py +2 -4
  74. infrahub/database/index.py +1 -1
  75. infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
  76. infrahub/dependencies/builder/constraint/schema/attribute_kind.py +8 -0
  77. infrahub/dependencies/builder/diff/data_check_synchronizer.py +2 -0
  78. infrahub/git/base.py +3 -3
  79. infrahub/git/integrator.py +1 -1
  80. infrahub/graphql/api/endpoints.py +12 -3
  81. infrahub/graphql/app.py +2 -2
  82. infrahub/graphql/auth/query_permission_checker/default_branch_checker.py +2 -17
  83. infrahub/graphql/auth/query_permission_checker/merge_operation_checker.py +1 -12
  84. infrahub/graphql/auth/query_permission_checker/object_permission_checker.py +6 -40
  85. infrahub/graphql/auth/query_permission_checker/super_admin_checker.py +5 -8
  86. infrahub/graphql/enums.py +2 -2
  87. infrahub/graphql/initialization.py +27 -8
  88. infrahub/graphql/manager.py +9 -3
  89. infrahub/graphql/models.py +6 -0
  90. infrahub/graphql/mutations/account.py +14 -10
  91. infrahub/graphql/mutations/computed_attribute.py +11 -22
  92. infrahub/graphql/mutations/diff.py +2 -0
  93. infrahub/graphql/mutations/main.py +5 -16
  94. infrahub/graphql/mutations/proposed_change.py +11 -20
  95. infrahub/graphql/mutations/resource_manager.py +6 -3
  96. infrahub/graphql/mutations/schema.py +8 -7
  97. infrahub/graphql/mutations/tasks.py +1 -1
  98. infrahub/graphql/permissions.py +3 -4
  99. infrahub/graphql/queries/account.py +2 -11
  100. infrahub/graphql/queries/resource_manager.py +21 -10
  101. infrahub/graphql/query.py +3 -1
  102. infrahub/graphql/resolvers/resolver.py +5 -1
  103. infrahub/graphql/types/task.py +14 -2
  104. infrahub/menu/generator.py +6 -18
  105. infrahub/message_bus/messages/event_node_mutated.py +2 -2
  106. infrahub/message_bus/operations/check/repository.py +2 -4
  107. infrahub/message_bus/operations/event/branch.py +2 -4
  108. infrahub/message_bus/operations/requests/proposed_change.py +1 -1
  109. infrahub/message_bus/operations/requests/repository.py +3 -5
  110. infrahub/message_bus/types.py +1 -1
  111. infrahub/permissions/__init__.py +12 -3
  112. infrahub/permissions/backend.py +2 -17
  113. infrahub/permissions/constants.py +12 -8
  114. infrahub/permissions/local_backend.py +5 -102
  115. infrahub/permissions/manager.py +135 -0
  116. infrahub/permissions/report.py +14 -25
  117. infrahub/permissions/types.py +6 -0
  118. infrahub/proposed_change/tasks.py +1 -1
  119. infrahub/task_manager/models.py +34 -5
  120. infrahub/task_manager/task.py +14 -6
  121. infrahub/visuals.py +1 -3
  122. infrahub_sdk/client.py +204 -43
  123. infrahub_sdk/ctl/cli_commands.py +106 -6
  124. infrahub_sdk/data.py +3 -2
  125. infrahub_sdk/graphql.py +5 -0
  126. infrahub_sdk/node.py +21 -2
  127. infrahub_sdk/queries.py +69 -0
  128. infrahub_sdk/schema/main.py +1 -0
  129. infrahub_sdk/testing/schemas/animal.py +1 -0
  130. infrahub_sdk/types.py +6 -0
  131. infrahub_sdk/utils.py +17 -0
  132. {infrahub_server-1.1.1.dist-info → infrahub_server-1.1.3.dist-info}/METADATA +1 -1
  133. {infrahub_server-1.1.1.dist-info → infrahub_server-1.1.3.dist-info}/RECORD +136 -131
  134. infrahub/core/diff/query/empty_roots.py +0 -33
  135. {infrahub_server-1.1.1.dist-info → infrahub_server-1.1.3.dist-info}/LICENSE.txt +0 -0
  136. {infrahub_server-1.1.1.dist-info → infrahub_server-1.1.3.dist-info}/WHEEL +0 -0
  137. {infrahub_server-1.1.1.dist-info → infrahub_server-1.1.3.dist-info}/entry_points.txt +0 -0
infrahub_sdk/client.py CHANGED
@@ -46,13 +46,13 @@ from .node import (
46
46
  )
47
47
  from .object_store import ObjectStore, ObjectStoreSync
48
48
  from .protocols_base import CoreNode, CoreNodeSync
49
- from .queries import get_commit_update_mutation
49
+ from .queries import QUERY_USER, get_commit_update_mutation
50
50
  from .query_groups import InfrahubGroupContext, InfrahubGroupContextSync
51
51
  from .schema import InfrahubSchema, InfrahubSchemaSync, NodeSchemaAPI
52
52
  from .store import NodeStore, NodeStoreSync
53
53
  from .timestamp import Timestamp
54
- from .types import AsyncRequester, HTTPMethod, SyncRequester
55
- from .utils import decode_json, is_valid_uuid
54
+ from .types import AsyncRequester, HTTPMethod, Order, SyncRequester
55
+ from .utils import decode_json, get_user_permissions, is_valid_uuid
56
56
 
57
57
  if TYPE_CHECKING:
58
58
  from types import TracebackType
@@ -272,6 +272,22 @@ class InfrahubClient(BaseClient):
272
272
  self._request_method: AsyncRequester = self.config.requester or self._default_request_method
273
273
  self.group_context = InfrahubGroupContext(self)
274
274
 
275
+ async def get_version(self) -> str:
276
+ """Return the Infrahub version."""
277
+ response = await self.execute_graphql(query="query { InfrahubInfo { version }}")
278
+ version = response.get("InfrahubInfo", {}).get("version", "")
279
+ return version
280
+
281
+ async def get_user(self) -> dict:
282
+ """Return user information"""
283
+ user_info = await self.execute_graphql(query=QUERY_USER)
284
+ return user_info
285
+
286
+ async def get_user_permissions(self) -> dict:
287
+ """Return user permissions"""
288
+ user_info = await self.get_user()
289
+ return get_user_permissions(user_info["AccountProfile"]["member_of_groups"]["edges"])
290
+
275
291
  @overload
276
292
  async def create(
277
293
  self,
@@ -525,6 +541,25 @@ class InfrahubClient(BaseClient):
525
541
 
526
542
  return ProcessRelationsNode(nodes=nodes, related_nodes=related_nodes)
527
543
 
544
+ async def count(
545
+ self,
546
+ kind: str | type[SchemaType],
547
+ at: Timestamp | None = None,
548
+ branch: str | None = None,
549
+ timeout: int | None = None,
550
+ ) -> int:
551
+ """Return the number of nodes of a given kind."""
552
+ schema = await self.schema.get(kind=kind, branch=branch)
553
+
554
+ branch = branch or self.default_branch
555
+ if at:
556
+ at = Timestamp(at)
557
+
558
+ response = await self.execute_graphql(
559
+ query=Query(query={schema.kind: {"count": None}}).render(), branch_name=branch, at=at, timeout=timeout
560
+ )
561
+ return int(response.get(schema.kind, {}).get("count", 0))
562
+
528
563
  @overload
529
564
  async def all(
530
565
  self,
@@ -540,6 +575,8 @@ class InfrahubClient(BaseClient):
540
575
  fragment: bool = ...,
541
576
  prefetch_relationships: bool = ...,
542
577
  property: bool = ...,
578
+ parallel: bool = ...,
579
+ order: Order | None = ...,
543
580
  ) -> list[SchemaType]: ...
544
581
 
545
582
  @overload
@@ -557,6 +594,8 @@ class InfrahubClient(BaseClient):
557
594
  fragment: bool = ...,
558
595
  prefetch_relationships: bool = ...,
559
596
  property: bool = ...,
597
+ parallel: bool = ...,
598
+ order: Order | None = ...,
560
599
  ) -> list[InfrahubNode]: ...
561
600
 
562
601
  async def all(
@@ -573,6 +612,8 @@ class InfrahubClient(BaseClient):
573
612
  fragment: bool = False,
574
613
  prefetch_relationships: bool = False,
575
614
  property: bool = False,
615
+ parallel: bool = False,
616
+ order: Order | None = None,
576
617
  ) -> list[InfrahubNode] | list[SchemaType]:
577
618
  """Retrieve all nodes of a given kind
578
619
 
@@ -588,6 +629,8 @@ class InfrahubClient(BaseClient):
588
629
  exclude (list[str], optional): List of attributes or relationships to exclude from the query.
589
630
  fragment (bool, optional): Flag to use GraphQL fragments for generic schemas.
590
631
  prefetch_relationships (bool, optional): Flag to indicate whether to prefetch related node data.
632
+ parallel (bool, optional): Whether to use parallel processing for the query.
633
+ order (Order, optional): Ordering related options. Setting `disable=True` enhances performances.
591
634
 
592
635
  Returns:
593
636
  list[InfrahubNode]: List of Nodes
@@ -605,6 +648,8 @@ class InfrahubClient(BaseClient):
605
648
  fragment=fragment,
606
649
  prefetch_relationships=prefetch_relationships,
607
650
  property=property,
651
+ parallel=parallel,
652
+ order=order,
608
653
  )
609
654
 
610
655
  @overload
@@ -623,6 +668,8 @@ class InfrahubClient(BaseClient):
623
668
  prefetch_relationships: bool = ...,
624
669
  partial_match: bool = ...,
625
670
  property: bool = ...,
671
+ parallel: bool = ...,
672
+ order: Order | None = ...,
626
673
  **kwargs: Any,
627
674
  ) -> list[SchemaType]: ...
628
675
 
@@ -642,6 +689,8 @@ class InfrahubClient(BaseClient):
642
689
  prefetch_relationships: bool = ...,
643
690
  partial_match: bool = ...,
644
691
  property: bool = ...,
692
+ parallel: bool = ...,
693
+ order: Order | None = ...,
645
694
  **kwargs: Any,
646
695
  ) -> list[InfrahubNode]: ...
647
696
 
@@ -660,6 +709,8 @@ class InfrahubClient(BaseClient):
660
709
  prefetch_relationships: bool = False,
661
710
  partial_match: bool = False,
662
711
  property: bool = False,
712
+ parallel: bool = False,
713
+ order: Order | None = None,
663
714
  **kwargs: Any,
664
715
  ) -> list[InfrahubNode] | list[SchemaType]:
665
716
  """Retrieve nodes of a given kind based on provided filters.
@@ -677,32 +728,27 @@ class InfrahubClient(BaseClient):
677
728
  fragment (bool, optional): Flag to use GraphQL fragments for generic schemas.
678
729
  prefetch_relationships (bool, optional): Flag to indicate whether to prefetch related node data.
679
730
  partial_match (bool, optional): Allow partial match of filter criteria for the query.
731
+ parallel (bool, optional): Whether to use parallel processing for the query.
732
+ order (Order, optional): Ordering related options. Setting `disable=True` enhances performances.
680
733
  **kwargs (Any): Additional filter criteria for the query.
681
734
 
682
735
  Returns:
683
736
  list[InfrahubNodeSync]: List of Nodes that match the given filters.
684
737
  """
685
- schema = await self.schema.get(kind=kind, branch=branch)
686
-
687
738
  branch = branch or self.default_branch
739
+ schema = await self.schema.get(kind=kind, branch=branch)
688
740
  if at:
689
741
  at = Timestamp(at)
690
742
 
691
743
  node = InfrahubNode(client=self, schema=schema, branch=branch)
692
744
  filters = kwargs
745
+ pagination_size = self.pagination_size
693
746
 
694
- nodes: list[InfrahubNode] = []
695
- related_nodes: list[InfrahubNode] = []
696
-
697
- has_remaining_items = True
698
- page_number = 1
699
-
700
- while has_remaining_items:
701
- page_offset = (page_number - 1) * self.pagination_size
702
-
747
+ async def process_page(page_offset: int, page_number: int) -> tuple[dict, ProcessRelationsNode]:
748
+ """Process a single page of results."""
703
749
  query_data = await InfrahubNode(client=self, schema=schema, branch=branch).generate_query_data(
704
750
  offset=offset or page_offset,
705
- limit=limit or self.pagination_size,
751
+ limit=limit or pagination_size,
706
752
  filters=filters,
707
753
  include=include,
708
754
  exclude=exclude,
@@ -710,6 +756,7 @@ class InfrahubClient(BaseClient):
710
756
  prefetch_relationships=prefetch_relationships,
711
757
  partial_match=partial_match,
712
758
  property=property,
759
+ order=order,
713
760
  )
714
761
  query = Query(query=query_data)
715
762
  response = await self.execute_graphql(
@@ -727,14 +774,48 @@ class InfrahubClient(BaseClient):
727
774
  prefetch_relationships=prefetch_relationships,
728
775
  timeout=timeout,
729
776
  )
730
- nodes.extend(process_result["nodes"])
731
- related_nodes.extend(process_result["related_nodes"])
777
+ return response, process_result
778
+
779
+ async def process_batch() -> tuple[list[InfrahubNode], list[InfrahubNode]]:
780
+ """Process queries in parallel mode."""
781
+ nodes = []
782
+ related_nodes = []
783
+ batch_process = await self.create_batch()
784
+ count = await self.count(kind=schema.kind)
785
+ total_pages = (count + pagination_size - 1) // pagination_size
786
+
787
+ for page_number in range(1, total_pages + 1):
788
+ page_offset = (page_number - 1) * pagination_size
789
+ batch_process.add(task=process_page, node=node, page_offset=page_offset, page_number=page_number)
732
790
 
733
- remaining_items = response[schema.kind].get("count", 0) - (page_offset + self.pagination_size)
734
- if remaining_items < 0 or offset is not None or limit is not None:
735
- has_remaining_items = False
791
+ async for _, response in batch_process.execute():
792
+ nodes.extend(response[1]["nodes"])
793
+ related_nodes.extend(response[1]["related_nodes"])
736
794
 
737
- page_number += 1
795
+ return nodes, related_nodes
796
+
797
+ async def process_non_batch() -> tuple[list[InfrahubNode], list[InfrahubNode]]:
798
+ """Process queries without parallel mode."""
799
+ nodes = []
800
+ related_nodes = []
801
+ has_remaining_items = True
802
+ page_number = 1
803
+
804
+ while has_remaining_items:
805
+ page_offset = (page_number - 1) * pagination_size
806
+ response, process_result = await process_page(page_offset, page_number)
807
+
808
+ nodes.extend(process_result["nodes"])
809
+ related_nodes.extend(process_result["related_nodes"])
810
+ remaining_items = response[schema.kind].get("count", 0) - (page_offset + pagination_size)
811
+ if remaining_items < 0 or offset is not None or limit is not None:
812
+ has_remaining_items = False
813
+ page_number += 1
814
+
815
+ return nodes, related_nodes
816
+
817
+ # Select parallel or non-parallel processing
818
+ nodes, related_nodes = await (process_batch() if parallel else process_non_batch())
738
819
 
739
820
  if populate_store:
740
821
  for node in nodes:
@@ -744,7 +825,6 @@ class InfrahubClient(BaseClient):
744
825
  for node in related_nodes:
745
826
  if node.id:
746
827
  self.store.set(key=node.id, node=node)
747
-
748
828
  return nodes
749
829
 
750
830
  def clone(self) -> InfrahubClient:
@@ -1425,6 +1505,22 @@ class InfrahubClientSync(BaseClient):
1425
1505
  self._request_method: SyncRequester = self.config.sync_requester or self._default_request_method
1426
1506
  self.group_context = InfrahubGroupContextSync(self)
1427
1507
 
1508
+ def get_version(self) -> str:
1509
+ """Return the Infrahub version."""
1510
+ response = self.execute_graphql(query="query { InfrahubInfo { version }}")
1511
+ version = response.get("InfrahubInfo", {}).get("version", "")
1512
+ return version
1513
+
1514
+ def get_user(self) -> dict:
1515
+ """Return user information"""
1516
+ user_info = self.execute_graphql(query=QUERY_USER)
1517
+ return user_info
1518
+
1519
+ def get_user_permissions(self) -> dict:
1520
+ """Return user permissions"""
1521
+ user_info = self.get_user()
1522
+ return get_user_permissions(user_info["AccountProfile"]["member_of_groups"]["edges"])
1523
+
1428
1524
  @overload
1429
1525
  def create(
1430
1526
  self,
@@ -1549,6 +1645,25 @@ class InfrahubClientSync(BaseClient):
1549
1645
 
1550
1646
  # TODO add a special method to execute mutation that will check if the method returned OK
1551
1647
 
1648
+ def count(
1649
+ self,
1650
+ kind: str | type[SchemaType],
1651
+ at: Timestamp | None = None,
1652
+ branch: str | None = None,
1653
+ timeout: int | None = None,
1654
+ ) -> int:
1655
+ """Return the number of nodes of a given kind."""
1656
+ schema = self.schema.get(kind=kind, branch=branch)
1657
+
1658
+ branch = branch or self.default_branch
1659
+ if at:
1660
+ at = Timestamp(at)
1661
+
1662
+ response = self.execute_graphql(
1663
+ query=Query(query={schema.kind: {"count": None}}).render(), branch_name=branch, at=at, timeout=timeout
1664
+ )
1665
+ return int(response.get(schema.kind, {}).get("count", 0))
1666
+
1552
1667
  @overload
1553
1668
  def all(
1554
1669
  self,
@@ -1564,6 +1679,8 @@ class InfrahubClientSync(BaseClient):
1564
1679
  fragment: bool = ...,
1565
1680
  prefetch_relationships: bool = ...,
1566
1681
  property: bool = ...,
1682
+ parallel: bool = ...,
1683
+ order: Order | None = ...,
1567
1684
  ) -> list[SchemaTypeSync]: ...
1568
1685
 
1569
1686
  @overload
@@ -1581,6 +1698,8 @@ class InfrahubClientSync(BaseClient):
1581
1698
  fragment: bool = ...,
1582
1699
  prefetch_relationships: bool = ...,
1583
1700
  property: bool = ...,
1701
+ parallel: bool = ...,
1702
+ order: Order | None = ...,
1584
1703
  ) -> list[InfrahubNodeSync]: ...
1585
1704
 
1586
1705
  def all(
@@ -1597,6 +1716,8 @@ class InfrahubClientSync(BaseClient):
1597
1716
  fragment: bool = False,
1598
1717
  prefetch_relationships: bool = False,
1599
1718
  property: bool = False,
1719
+ parallel: bool = False,
1720
+ order: Order | None = None,
1600
1721
  ) -> list[InfrahubNodeSync] | list[SchemaTypeSync]:
1601
1722
  """Retrieve all nodes of a given kind
1602
1723
 
@@ -1612,6 +1733,8 @@ class InfrahubClientSync(BaseClient):
1612
1733
  exclude (list[str], optional): List of attributes or relationships to exclude from the query.
1613
1734
  fragment (bool, optional): Flag to use GraphQL fragments for generic schemas.
1614
1735
  prefetch_relationships (bool, optional): Flag to indicate whether to prefetch related node data.
1736
+ parallel (bool, optional): Whether to use parallel processing for the query.
1737
+ order (Order, optional): Ordering related options. Setting `disable=True` enhances performances.
1615
1738
 
1616
1739
  Returns:
1617
1740
  list[InfrahubNodeSync]: List of Nodes
@@ -1629,6 +1752,8 @@ class InfrahubClientSync(BaseClient):
1629
1752
  fragment=fragment,
1630
1753
  prefetch_relationships=prefetch_relationships,
1631
1754
  property=property,
1755
+ parallel=parallel,
1756
+ order=order,
1632
1757
  )
1633
1758
 
1634
1759
  def _process_nodes_and_relationships(
@@ -1682,6 +1807,8 @@ class InfrahubClientSync(BaseClient):
1682
1807
  prefetch_relationships: bool = ...,
1683
1808
  partial_match: bool = ...,
1684
1809
  property: bool = ...,
1810
+ parallel: bool = ...,
1811
+ order: Order | None = ...,
1685
1812
  **kwargs: Any,
1686
1813
  ) -> list[SchemaTypeSync]: ...
1687
1814
 
@@ -1701,6 +1828,8 @@ class InfrahubClientSync(BaseClient):
1701
1828
  prefetch_relationships: bool = ...,
1702
1829
  partial_match: bool = ...,
1703
1830
  property: bool = ...,
1831
+ parallel: bool = ...,
1832
+ order: Order | None = ...,
1704
1833
  **kwargs: Any,
1705
1834
  ) -> list[InfrahubNodeSync]: ...
1706
1835
 
@@ -1719,6 +1848,8 @@ class InfrahubClientSync(BaseClient):
1719
1848
  prefetch_relationships: bool = False,
1720
1849
  partial_match: bool = False,
1721
1850
  property: bool = False,
1851
+ parallel: bool = False,
1852
+ order: Order | None = None,
1722
1853
  **kwargs: Any,
1723
1854
  ) -> list[InfrahubNodeSync] | list[SchemaTypeSync]:
1724
1855
  """Retrieve nodes of a given kind based on provided filters.
@@ -1736,32 +1867,26 @@ class InfrahubClientSync(BaseClient):
1736
1867
  fragment (bool, optional): Flag to use GraphQL fragments for generic schemas.
1737
1868
  prefetch_relationships (bool, optional): Flag to indicate whether to prefetch related node data.
1738
1869
  partial_match (bool, optional): Allow partial match of filter criteria for the query.
1870
+ parallel (bool, optional): Whether to use parallel processing for the query.
1871
+ order (Order, optional): Ordering related options. Setting `disable=True` enhances performances.
1739
1872
  **kwargs (Any): Additional filter criteria for the query.
1740
1873
 
1741
1874
  Returns:
1742
1875
  list[InfrahubNodeSync]: List of Nodes that match the given filters.
1743
1876
  """
1744
- schema = self.schema.get(kind=kind, branch=branch)
1745
-
1746
1877
  branch = branch or self.default_branch
1878
+ schema = self.schema.get(kind=kind, branch=branch)
1879
+ node = InfrahubNodeSync(client=self, schema=schema, branch=branch)
1747
1880
  if at:
1748
1881
  at = Timestamp(at)
1749
-
1750
- node = InfrahubNodeSync(client=self, schema=schema, branch=branch)
1751
1882
  filters = kwargs
1883
+ pagination_size = self.pagination_size
1752
1884
 
1753
- nodes: list[InfrahubNodeSync] = []
1754
- related_nodes: list[InfrahubNodeSync] = []
1755
-
1756
- has_remaining_items = True
1757
- page_number = 1
1758
-
1759
- while has_remaining_items:
1760
- page_offset = (page_number - 1) * self.pagination_size
1761
-
1885
+ def process_page(page_offset: int, page_number: int) -> tuple[dict, ProcessRelationsNodeSync]:
1886
+ """Process a single page of results."""
1762
1887
  query_data = InfrahubNodeSync(client=self, schema=schema, branch=branch).generate_query_data(
1763
1888
  offset=offset or page_offset,
1764
- limit=limit or self.pagination_size,
1889
+ limit=limit or pagination_size,
1765
1890
  filters=filters,
1766
1891
  include=include,
1767
1892
  exclude=exclude,
@@ -1769,6 +1894,7 @@ class InfrahubClientSync(BaseClient):
1769
1894
  prefetch_relationships=prefetch_relationships,
1770
1895
  partial_match=partial_match,
1771
1896
  property=property,
1897
+ order=order,
1772
1898
  )
1773
1899
  query = Query(query=query_data)
1774
1900
  response = self.execute_graphql(
@@ -1786,14 +1912,50 @@ class InfrahubClientSync(BaseClient):
1786
1912
  prefetch_relationships=prefetch_relationships,
1787
1913
  timeout=timeout,
1788
1914
  )
1789
- nodes.extend(process_result["nodes"])
1790
- related_nodes.extend(process_result["related_nodes"])
1915
+ return response, process_result
1916
+
1917
+ def process_batch() -> tuple[list[InfrahubNodeSync], list[InfrahubNodeSync]]:
1918
+ """Process queries in parallel mode."""
1919
+ nodes = []
1920
+ related_nodes = []
1921
+ batch_process = self.create_batch()
1922
+
1923
+ count = self.count(kind=schema.kind)
1924
+ total_pages = (count + pagination_size - 1) // pagination_size
1791
1925
 
1792
- remaining_items = response[schema.kind].get("count", 0) - (page_offset + self.pagination_size)
1793
- if remaining_items < 0 or offset is not None or limit is not None:
1794
- has_remaining_items = False
1926
+ for page_number in range(1, total_pages + 1):
1927
+ page_offset = (page_number - 1) * pagination_size
1928
+ batch_process.add(task=process_page, node=node, page_offset=page_offset, page_number=page_number)
1795
1929
 
1796
- page_number += 1
1930
+ for _, response in batch_process.execute():
1931
+ nodes.extend(response[1]["nodes"])
1932
+ related_nodes.extend(response[1]["related_nodes"])
1933
+
1934
+ return nodes, related_nodes
1935
+
1936
+ def process_non_batch() -> tuple[list[InfrahubNodeSync], list[InfrahubNodeSync]]:
1937
+ """Process queries without parallel mode."""
1938
+ nodes = []
1939
+ related_nodes = []
1940
+ has_remaining_items = True
1941
+ page_number = 1
1942
+
1943
+ while has_remaining_items:
1944
+ page_offset = (page_number - 1) * pagination_size
1945
+ response, process_result = process_page(page_offset, page_number)
1946
+
1947
+ nodes.extend(process_result["nodes"])
1948
+ related_nodes.extend(process_result["related_nodes"])
1949
+
1950
+ remaining_items = response[schema.kind].get("count", 0) - (page_offset + pagination_size)
1951
+ if remaining_items < 0 or offset is not None or limit is not None:
1952
+ has_remaining_items = False
1953
+ page_number += 1
1954
+
1955
+ return nodes, related_nodes
1956
+
1957
+ # Select parallel or non-parallel processing
1958
+ nodes, related_nodes = process_batch() if parallel else process_non_batch()
1797
1959
 
1798
1960
  if populate_store:
1799
1961
  for node in nodes:
@@ -1803,7 +1965,6 @@ class InfrahubClientSync(BaseClient):
1803
1965
  for node in related_nodes:
1804
1966
  if node.id:
1805
1967
  self.store.set(key=node.id, node=node)
1806
-
1807
1968
  return nodes
1808
1969
 
1809
1970
  @overload
@@ -4,6 +4,7 @@ import asyncio
4
4
  import functools
5
5
  import importlib
6
6
  import logging
7
+ import platform
7
8
  import sys
8
9
  from pathlib import Path
9
10
  from typing import TYPE_CHECKING, Any, Callable
@@ -12,7 +13,11 @@ import jinja2
12
13
  import typer
13
14
  import ujson
14
15
  from rich.console import Console
16
+ from rich.layout import Layout
15
17
  from rich.logging import RichHandler
18
+ from rich.panel import Panel
19
+ from rich.pretty import Pretty
20
+ from rich.table import Table
16
21
  from rich.traceback import Traceback
17
22
 
18
23
  from .. import __version__ as sdk_version
@@ -392,11 +397,106 @@ def protocols(
392
397
 
393
398
  @app.command(name="version")
394
399
  @catch_exception(console=console)
395
- def version(_: str = CONFIG_PARAM) -> None:
396
- """Display the version of Infrahub and the version of the Python SDK in use."""
400
+ def version() -> None:
401
+ """Display the version of Python and the version of the Python SDK in use."""
397
402
 
398
- client = initialize_client_sync()
399
- response = client.execute_graphql(query="query { InfrahubInfo { version }}")
403
+ console.print(f"Python: {platform.python_version()}\nPython SDK: v{sdk_version}")
400
404
 
401
- infrahub_version = response["InfrahubInfo"]["version"]
402
- console.print(f"Infrahub: v{infrahub_version}\nPython SDK: v{sdk_version}")
405
+
406
+ @app.command(name="info")
407
+ @catch_exception(console=console)
408
+ def info(detail: bool = typer.Option(False, help="Display detailed information."), _: str = CONFIG_PARAM) -> None: # noqa: PLR0915
409
+ """Display the status of the Python SDK."""
410
+
411
+ info: dict[str, Any] = {
412
+ "error": None,
413
+ "status": ":x:",
414
+ "infrahub_version": "N/A",
415
+ "user_info": {},
416
+ "groups": {},
417
+ }
418
+ try:
419
+ client = initialize_client_sync()
420
+ info["infrahub_version"] = client.get_version()
421
+ info["user_info"] = client.get_user()
422
+ info["status"] = ":white_heavy_check_mark:"
423
+ info["groups"] = client.get_user_permissions()
424
+ except Exception as e:
425
+ info["error"] = f"{e!s} ({e.__class__.__name__})"
426
+
427
+ if detail:
428
+ layout = Layout()
429
+
430
+ # Layout structure
431
+ new_console = Console(height=45)
432
+ layout = Layout()
433
+ layout.split_column(
434
+ Layout(name="body", ratio=1),
435
+ )
436
+ layout["body"].split_row(
437
+ Layout(name="left"),
438
+ Layout(name="right"),
439
+ )
440
+
441
+ layout["left"].split_column(
442
+ Layout(name="connection_status", size=7),
443
+ Layout(name="client_info", ratio=1),
444
+ )
445
+
446
+ layout["right"].split_column(
447
+ Layout(name="version_info", size=7),
448
+ Layout(name="infrahub_info", ratio=1),
449
+ )
450
+
451
+ # Connection status panel
452
+ connection_status = Table(show_header=False, box=None)
453
+ connection_status.add_row("Server Address:", client.config.address)
454
+ connection_status.add_row("Status:", info["status"])
455
+ if info["error"]:
456
+ connection_status.add_row("Error Reason:", info["error"])
457
+ layout["connection_status"].update(Panel(connection_status, title="Connection Status"))
458
+
459
+ # Version information panel
460
+ version_info = Table(show_header=False, box=None)
461
+ version_info.add_row("Python Version:", platform.python_version())
462
+ version_info.add_row("Infrahub Version", info["infrahub_version"])
463
+ version_info.add_row("Infrahub SDK:", sdk_version)
464
+ layout["version_info"].update(Panel(version_info, title="Version Information"))
465
+
466
+ # SDK client configuration panel
467
+ pretty_model = Pretty(client.config.model_dump(), expand_all=True)
468
+ layout["client_info"].update(Panel(pretty_model, title="Client Info"))
469
+
470
+ # Infrahub information planel
471
+ infrahub_info = Table(show_header=False, box=None)
472
+ if info["user_info"]:
473
+ infrahub_info.add_row("User:", info["user_info"]["AccountProfile"]["display_label"])
474
+ infrahub_info.add_row("Description:", info["user_info"]["AccountProfile"]["description"]["value"])
475
+ infrahub_info.add_row("Status:", info["user_info"]["AccountProfile"]["status"]["label"])
476
+ infrahub_info.add_row(
477
+ "Number of Groups:", str(info["user_info"]["AccountProfile"]["member_of_groups"]["count"])
478
+ )
479
+
480
+ if groups := info["groups"]:
481
+ infrahub_info.add_row("Groups:", "")
482
+ for group, roles in groups.items():
483
+ infrahub_info.add_row("", group, ", ".join(roles))
484
+
485
+ layout["infrahub_info"].update(Panel(infrahub_info, title="Infrahub Info"))
486
+
487
+ new_console.print(layout)
488
+ else:
489
+ # Simple output
490
+ table = Table(show_header=False, box=None)
491
+ table.add_row("Address:", client.config.address)
492
+ table.add_row("Connection Status:", info["status"])
493
+ if info["error"]:
494
+ table.add_row("Connection Error:", info["error"])
495
+
496
+ table.add_row("Python Version:", platform.python_version())
497
+ table.add_row("SDK Version:", sdk_version)
498
+ table.add_row("Infrahub Version:", info["infrahub_version"])
499
+ if account := info["user_info"].get("AccountProfile"):
500
+ table.add_row("User:", account["display_label"])
501
+
502
+ console.print(table)
infrahub_sdk/data.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from pydantic import BaseModel, ConfigDict, Field
4
4
 
5
- from .node import InfrahubNode # noqa: TCH001
5
+ from .node import InfrahubNode # noqa: TC001
6
6
 
7
7
 
8
8
  class RepositoryBranchInfo(BaseModel):
@@ -13,7 +13,8 @@ class RepositoryData(BaseModel):
13
13
  model_config = ConfigDict(arbitrary_types_allowed=True)
14
14
  repository: InfrahubNode = Field(..., description="InfrahubNode representing a Repository")
15
15
  branches: dict[str, str] = Field(
16
- ..., description="Dictionary with the name of the branch as the key and the active commit id as the value"
16
+ ...,
17
+ description="Dictionary with the name of the branch as the key and the active commit id as the value",
17
18
  )
18
19
 
19
20
  branch_info: dict[str, RepositoryBranchInfo] = Field(default_factory=dict)
infrahub_sdk/graphql.py CHANGED
@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
+ from pydantic import BaseModel
6
+
5
7
  VARIABLE_TYPE_MAPPING = ((str, "String!"), (int, "Int!"), (float, "Float!"), (bool, "Boolean!"))
6
8
 
7
9
 
@@ -15,6 +17,9 @@ def convert_to_graphql_as_string(value: str | bool | list) -> str:
15
17
  if isinstance(value, list):
16
18
  values_as_string = [convert_to_graphql_as_string(item) for item in value]
17
19
  return "[" + ", ".join(values_as_string) + "]"
20
+ if isinstance(value, BaseModel):
21
+ data = value.model_dump()
22
+ return "{ " + ", ".join(f"{key}: {convert_to_graphql_as_string(val)}" for key, val in data.items()) + " }"
18
23
 
19
24
  return str(value)
20
25