infrahub-server 1.1.6__py3-none-any.whl → 1.1.8__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 (97) hide show
  1. infrahub/core/attribute.py +4 -1
  2. infrahub/core/branch/tasks.py +7 -4
  3. infrahub/core/diff/combiner.py +11 -7
  4. infrahub/core/diff/coordinator.py +49 -70
  5. infrahub/core/diff/data_check_synchronizer.py +86 -7
  6. infrahub/core/diff/enricher/aggregated.py +3 -3
  7. infrahub/core/diff/enricher/cardinality_one.py +6 -6
  8. infrahub/core/diff/enricher/hierarchy.py +17 -4
  9. infrahub/core/diff/enricher/labels.py +18 -3
  10. infrahub/core/diff/enricher/path_identifier.py +7 -8
  11. infrahub/core/diff/merger/merger.py +5 -3
  12. infrahub/core/diff/model/path.py +66 -25
  13. infrahub/core/diff/parent_node_adder.py +78 -0
  14. infrahub/core/diff/payload_builder.py +13 -2
  15. infrahub/core/diff/query/all_conflicts.py +5 -2
  16. infrahub/core/diff/query/diff_get.py +2 -1
  17. infrahub/core/diff/query/field_specifiers.py +2 -0
  18. infrahub/core/diff/query/field_summary.py +2 -1
  19. infrahub/core/diff/query/filters.py +12 -1
  20. infrahub/core/diff/query/has_conflicts_query.py +5 -2
  21. infrahub/core/diff/query/{drop_tracking_id.py → merge_tracking_id.py} +3 -3
  22. infrahub/core/diff/query/roots_metadata.py +8 -1
  23. infrahub/core/diff/query/save.py +230 -139
  24. infrahub/core/diff/query/summary_counts_enricher.py +267 -0
  25. infrahub/core/diff/query/time_range_query.py +2 -1
  26. infrahub/core/diff/query_parser.py +49 -24
  27. infrahub/core/diff/repository/deserializer.py +31 -27
  28. infrahub/core/diff/repository/repository.py +215 -41
  29. infrahub/core/diff/tasks.py +4 -4
  30. infrahub/core/graph/__init__.py +1 -1
  31. infrahub/core/graph/index.py +3 -0
  32. infrahub/core/migrations/graph/__init__.py +4 -0
  33. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +256 -0
  34. infrahub/core/migrations/graph/m020_duplicate_edges.py +160 -0
  35. infrahub/core/migrations/query/node_duplicate.py +38 -18
  36. infrahub/core/migrations/schema/node_remove.py +26 -12
  37. infrahub/core/migrations/shared.py +10 -8
  38. infrahub/core/node/__init__.py +19 -9
  39. infrahub/core/node/constraints/grouped_uniqueness.py +25 -5
  40. infrahub/core/node/ipam.py +6 -1
  41. infrahub/core/node/permissions.py +4 -0
  42. infrahub/core/query/attribute.py +2 -0
  43. infrahub/core/query/diff.py +41 -3
  44. infrahub/core/query/node.py +74 -21
  45. infrahub/core/query/relationship.py +107 -17
  46. infrahub/core/query/resource_manager.py +5 -1
  47. infrahub/core/relationship/model.py +8 -12
  48. infrahub/core/schema/definitions/core.py +1 -0
  49. infrahub/core/utils.py +1 -0
  50. infrahub/core/validators/uniqueness/query.py +20 -17
  51. infrahub/database/__init__.py +14 -0
  52. infrahub/dependencies/builder/constraint/grouped/node_runner.py +0 -2
  53. infrahub/dependencies/builder/diff/coordinator.py +0 -2
  54. infrahub/dependencies/builder/diff/deserializer.py +3 -1
  55. infrahub/dependencies/builder/diff/enricher/hierarchy.py +3 -1
  56. infrahub/dependencies/builder/diff/parent_node_adder.py +8 -0
  57. infrahub/graphql/mutations/computed_attribute.py +3 -1
  58. infrahub/graphql/mutations/diff.py +41 -10
  59. infrahub/graphql/mutations/main.py +11 -6
  60. infrahub/graphql/mutations/relationship.py +29 -1
  61. infrahub/graphql/mutations/resource_manager.py +3 -3
  62. infrahub/graphql/mutations/tasks.py +6 -3
  63. infrahub/graphql/queries/resource_manager.py +7 -3
  64. infrahub/permissions/__init__.py +2 -1
  65. infrahub/permissions/types.py +26 -0
  66. infrahub_sdk/client.py +10 -2
  67. infrahub_sdk/config.py +3 -0
  68. infrahub_sdk/ctl/check.py +3 -3
  69. infrahub_sdk/ctl/cli_commands.py +16 -11
  70. infrahub_sdk/ctl/exceptions.py +0 -6
  71. infrahub_sdk/ctl/exporter.py +1 -1
  72. infrahub_sdk/ctl/generator.py +5 -5
  73. infrahub_sdk/ctl/importer.py +3 -2
  74. infrahub_sdk/ctl/menu.py +1 -1
  75. infrahub_sdk/ctl/object.py +1 -1
  76. infrahub_sdk/ctl/repository.py +23 -15
  77. infrahub_sdk/ctl/schema.py +2 -2
  78. infrahub_sdk/ctl/utils.py +4 -3
  79. infrahub_sdk/ctl/validate.py +2 -1
  80. infrahub_sdk/exceptions.py +12 -0
  81. infrahub_sdk/generator.py +3 -0
  82. infrahub_sdk/node.py +7 -4
  83. infrahub_sdk/testing/schemas/animal.py +9 -0
  84. infrahub_sdk/utils.py +11 -1
  85. infrahub_sdk/yaml.py +2 -3
  86. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/METADATA +41 -7
  87. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/RECORD +94 -91
  88. infrahub_testcontainers/container.py +12 -3
  89. infrahub_testcontainers/docker-compose.test.yml +22 -3
  90. infrahub_testcontainers/haproxy.cfg +43 -0
  91. infrahub_testcontainers/helpers.py +85 -1
  92. infrahub/core/diff/enricher/summary_counts.py +0 -105
  93. infrahub/dependencies/builder/diff/enricher/summary_counts.py +0 -8
  94. infrahub_sdk/ctl/_file.py +0 -13
  95. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/LICENSE.txt +0 -0
  96. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/WHEEL +0 -0
  97. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/entry_points.txt +0 -0
infrahub_sdk/client.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import copy
5
5
  import logging
6
+ import time
6
7
  from collections.abc import Coroutine, MutableMapping
7
8
  from functools import wraps
8
9
  from time import sleep
@@ -38,6 +39,7 @@ from .exceptions import (
38
39
  NodeNotFoundError,
39
40
  ServerNotReachableError,
40
41
  ServerNotResponsiveError,
42
+ URLNotFoundError,
41
43
  )
42
44
  from .graphql import Mutation, Query
43
45
  from .node import (
@@ -878,7 +880,8 @@ class InfrahubClient(BaseClient):
878
880
 
879
881
  retry = True
880
882
  resp = None
881
- while retry:
883
+ start_time = time.time()
884
+ while retry and time.time() - start_time < self.config.max_retry_duration:
882
885
  retry = self.retry_on_failure
883
886
  try:
884
887
  resp = await self._post(url=url, payload=payload, headers=headers, timeout=timeout)
@@ -902,6 +905,8 @@ class InfrahubClient(BaseClient):
902
905
  errors = response.get("errors", [])
903
906
  messages = [error.get("message") for error in errors]
904
907
  raise AuthenticationError(" | ".join(messages)) from exc
908
+ if exc.response.status_code == 404:
909
+ raise URLNotFoundError(url=url)
905
910
 
906
911
  if not resp:
907
912
  raise Error("Unexpected situation, resp hasn't been initialized.")
@@ -1613,7 +1618,8 @@ class InfrahubClientSync(BaseClient):
1613
1618
 
1614
1619
  retry = True
1615
1620
  resp = None
1616
- while retry:
1621
+ start_time = time.time()
1622
+ while retry and time.time() - start_time < self.config.max_retry_duration:
1617
1623
  retry = self.retry_on_failure
1618
1624
  try:
1619
1625
  resp = self._post(url=url, payload=payload, headers=headers, timeout=timeout)
@@ -1637,6 +1643,8 @@ class InfrahubClientSync(BaseClient):
1637
1643
  errors = response.get("errors", [])
1638
1644
  messages = [error.get("message") for error in errors]
1639
1645
  raise AuthenticationError(" | ".join(messages)) from exc
1646
+ if exc.response.status_code == 404:
1647
+ raise URLNotFoundError(url=url)
1640
1648
 
1641
1649
  if not resp:
1642
1650
  raise Error("Unexpected situation, resp hasn't been initialized.")
infrahub_sdk/config.py CHANGED
@@ -56,6 +56,9 @@ class ConfigBase(BaseSettings):
56
56
  pagination_size: int = Field(default=50, description="Page size for queries to the server")
57
57
  retry_delay: int = Field(default=5, description="Number of seconds to wait until attempting a retry.")
58
58
  retry_on_failure: bool = Field(default=False, description="Retry operation in case of failure")
59
+ max_retry_duration: int = Field(
60
+ default=300, description="Maximum duration until we stop attempting to retry if enabled."
61
+ )
59
62
  schema_converge_timeout: int = Field(
60
63
  default=60, description="Number of seconds to wait for schema to have converged"
61
64
  )
infrahub_sdk/ctl/check.py CHANGED
@@ -5,7 +5,7 @@ import sys
5
5
  from asyncio import run as aiorun
6
6
  from dataclasses import dataclass
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING
8
+ from typing import TYPE_CHECKING, Optional
9
9
 
10
10
  import typer
11
11
  from rich.console import Console
@@ -50,8 +50,8 @@ def run(
50
50
  format_json: bool,
51
51
  list_available: bool,
52
52
  variables: dict[str, str],
53
- name: str | None = None,
54
- branch: str | None = None,
53
+ name: Optional[str] = None,
54
+ branch: Optional[str] = None,
55
55
  ) -> None:
56
56
  """Locate and execute all checks under the defined path."""
57
57
 
@@ -7,7 +7,7 @@ import logging
7
7
  import platform
8
8
  import sys
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, Callable
10
+ from typing import TYPE_CHECKING, Any, Callable, Optional
11
11
 
12
12
  import jinja2
13
13
  import typer
@@ -74,13 +74,13 @@ console = Console()
74
74
  @catch_exception(console=console)
75
75
  def check(
76
76
  check_name: str = typer.Argument(default="", help="Name of the Python check"),
77
- branch: str | None = None,
77
+ branch: Optional[str] = None,
78
78
  path: str = typer.Option(".", help="Root directory"),
79
79
  debug: bool = False,
80
80
  format_json: bool = False,
81
81
  _: str = CONFIG_PARAM,
82
82
  list_available: bool = typer.Option(False, "--list", help="Show available Python checks"),
83
- variables: list[str] | None = typer.Argument(
83
+ variables: Optional[list[str]] = typer.Argument(
84
84
  None, help="Variables to pass along with the query. Format key=value key=value."
85
85
  ),
86
86
  ) -> None:
@@ -102,12 +102,12 @@ def check(
102
102
  @catch_exception(console=console)
103
103
  async def generator(
104
104
  generator_name: str = typer.Argument(default="", help="Name of the Generator"),
105
- branch: str | None = None,
105
+ branch: Optional[str] = None,
106
106
  path: str = typer.Option(".", help="Root directory"),
107
107
  debug: bool = False,
108
108
  _: str = CONFIG_PARAM,
109
109
  list_available: bool = typer.Option(False, "--list", help="Show available Generators"),
110
- variables: list[str] | None = typer.Argument(
110
+ variables: Optional[list[str]] = typer.Argument(
111
111
  None, help="Variables to pass along with the query. Format key=value key=value."
112
112
  ),
113
113
  ) -> None:
@@ -129,14 +129,14 @@ async def run(
129
129
  method: str = "run",
130
130
  debug: bool = False,
131
131
  _: str = CONFIG_PARAM,
132
- branch: str = typer.Option("main", help="Branch on which to run the script."),
133
- concurrent: int | None = typer.Option(
132
+ branch: str = typer.Option(None, help="Branch on which to run the script."),
133
+ concurrent: Optional[int] = typer.Option(
134
134
  None,
135
135
  help="Maximum number of requests to execute at the same time.",
136
136
  envvar="INFRAHUB_MAX_CONCURRENT_EXECUTION",
137
137
  ),
138
138
  timeout: int = typer.Option(60, help="Timeout in sec", envvar="INFRAHUB_TIMEOUT"),
139
- variables: list[str] | None = typer.Argument(
139
+ variables: Optional[list[str]] = typer.Argument(
140
140
  None, help="Variables to pass along with the query. Format key=value key=value."
141
141
  ),
142
142
  ) -> None:
@@ -259,7 +259,7 @@ def _run_transform(
259
259
  @catch_exception(console=console)
260
260
  def render(
261
261
  transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False),
262
- variables: list[str] | None = typer.Argument(
262
+ variables: Optional[list[str]] = typer.Argument(
263
263
  None, help="Variables to pass along with the query. Format key=value key=value."
264
264
  ),
265
265
  branch: str = typer.Option(None, help="Branch on which to render the transform."),
@@ -309,7 +309,7 @@ def render(
309
309
  @catch_exception(console=console)
310
310
  def transform(
311
311
  transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False),
312
- variables: list[str] | None = typer.Argument(
312
+ variables: Optional[list[str]] = typer.Argument(
313
313
  None, help="Variables to pass along with the query. Format key=value key=value."
314
314
  ),
315
315
  branch: str = typer.Option(None, help="Branch on which to run the transformation"),
@@ -352,7 +352,11 @@ def transform(
352
352
  # Run Transform
353
353
  result = asyncio.run(transform.run(data=data))
354
354
 
355
- json_string = ujson.dumps(result, indent=2, sort_keys=True)
355
+ if isinstance(result, str):
356
+ json_string = result
357
+ else:
358
+ json_string = ujson.dumps(result, indent=2, sort_keys=True)
359
+
356
360
  if out:
357
361
  write_to_file(Path(out), json_string)
358
362
  else:
@@ -383,6 +387,7 @@ def protocols(
383
387
 
384
388
  else:
385
389
  client = initialize_client_sync()
390
+ branch = branch or client.default_branch
386
391
  schema.update(client.schema.fetch(branch=branch))
387
392
 
388
393
  code_generator = CodeGenerator(schema=schema)
@@ -6,9 +6,3 @@ class QueryNotFoundError(Error):
6
6
  def __init__(self, name: str, message: str = ""):
7
7
  self.message = message or f"The requested query '{name}' was not found."
8
8
  super().__init__(self.message)
9
-
10
-
11
- class FileNotValidError(Error):
12
- def __init__(self, name: str, message: str = ""):
13
- self.message = message or f"Cannot parse '{name}' content."
14
- super().__init__(self.message)
@@ -22,7 +22,7 @@ def dump(
22
22
  directory: Path = typer.Option(directory_name_with_timestamp, help="Directory path to store export"),
23
23
  quiet: bool = typer.Option(False, help="No console output"),
24
24
  _: str = CONFIG_PARAM,
25
- branch: str = typer.Option("main", help="Branch from which to export"),
25
+ branch: str = typer.Option(None, help="Branch from which to export"),
26
26
  concurrent: int = typer.Option(
27
27
  4,
28
28
  help="Maximum number of requests to execute at the same time.",
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
- from typing import TYPE_CHECKING
4
+ from typing import TYPE_CHECKING, Optional
5
5
 
6
6
  import typer
7
7
  from rich.console import Console
@@ -9,7 +9,7 @@ from rich.console import Console
9
9
  from ..ctl import config
10
10
  from ..ctl.client import initialize_client
11
11
  from ..ctl.repository import get_repository_config
12
- from ..ctl.utils import execute_graphql_query, parse_cli_vars
12
+ from ..ctl.utils import execute_graphql_query, init_logging, parse_cli_vars
13
13
  from ..exceptions import ModuleImportError
14
14
  from ..node import InfrahubNode
15
15
 
@@ -20,11 +20,12 @@ if TYPE_CHECKING:
20
20
  async def run(
21
21
  generator_name: str,
22
22
  path: str, # noqa: ARG001
23
- debug: bool, # noqa: ARG001
23
+ debug: bool,
24
24
  list_available: bool,
25
25
  branch: str | None = None,
26
- variables: list[str] | None = None,
26
+ variables: Optional[list[str]] = None,
27
27
  ) -> None:
28
+ init_logging(debug=debug)
28
29
  repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE))
29
30
 
30
31
  if list_available or not generator_name:
@@ -34,7 +35,6 @@ async def run(
34
35
  generator_config = repository_config.get_generator_definition(name=generator_name)
35
36
 
36
37
  console = Console()
37
-
38
38
  relative_path = str(generator_config.file_path.parent) if generator_config.file_path.parent != Path() else None
39
39
 
40
40
  try:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from asyncio import run as aiorun
4
4
  from pathlib import Path
5
+ from typing import Optional
5
6
 
6
7
  import typer
7
8
  from rich.console import Console
@@ -25,8 +26,8 @@ def load(
25
26
  ),
26
27
  quiet: bool = typer.Option(False, help="No console output"),
27
28
  _: str = CONFIG_PARAM,
28
- branch: str = typer.Option("main", help="Branch from which to export"),
29
- concurrent: int | None = typer.Option(
29
+ branch: str = typer.Option(None, help="Branch from which to export"),
30
+ concurrent: Optional[int] = typer.Option(
30
31
  None,
31
32
  help="Maximum number of requests to execute at the same time.",
32
33
  envvar="INFRAHUB_MAX_CONCURRENT_EXECUTION",
infrahub_sdk/ctl/menu.py CHANGED
@@ -27,7 +27,7 @@ def callback() -> None:
27
27
  async def load(
28
28
  menus: list[Path],
29
29
  debug: bool = False,
30
- branch: str = typer.Option("main", help="Branch on which to load the menu."),
30
+ branch: str = typer.Option(None, help="Branch on which to load the menu."),
31
31
  _: str = CONFIG_PARAM,
32
32
  ) -> None:
33
33
  """Load one or multiple menu files into Infrahub."""
@@ -27,7 +27,7 @@ def callback() -> None:
27
27
  async def load(
28
28
  paths: list[Path],
29
29
  debug: bool = False,
30
- branch: str = typer.Option("main", help="Branch on which to load the objects."),
30
+ branch: str = typer.Option(None, help="Branch on which to load the objects."),
31
31
  _: str = CONFIG_PARAM,
32
32
  ) -> None:
33
33
  """Load one or multiple objects files into Infrahub."""
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
+ from typing import Optional
4
5
 
5
6
  import typer
6
7
  import yaml
@@ -8,15 +9,14 @@ from pydantic import ValidationError
8
9
  from rich.console import Console
9
10
  from rich.table import Table
10
11
 
11
- from infrahub_sdk.ctl.client import initialize_client
12
-
13
12
  from ..async_typer import AsyncTyper
14
- from ..ctl.exceptions import FileNotValidError
15
- from ..ctl.utils import init_logging
13
+ from ..exceptions import FileNotValidError
16
14
  from ..graphql import Mutation, Query
17
15
  from ..schema.repository import InfrahubRepositoryConfig
18
- from ._file import read_file
16
+ from ..utils import read_file
17
+ from .client import initialize_client
19
18
  from .parameters import CONFIG_PARAM
19
+ from .utils import init_logging
20
20
 
21
21
  app = AsyncTyper()
22
22
  console = Console()
@@ -69,12 +69,11 @@ async def add(
69
69
  name: str,
70
70
  location: str,
71
71
  description: str = "",
72
- username: str | None = None,
72
+ username: Optional[str] = None,
73
73
  password: str = "",
74
- commit: str = "",
74
+ ref: str = "",
75
75
  read_only: bool = False,
76
76
  debug: bool = False,
77
- branch: str = typer.Option("main", help="Branch on which to add the repository."),
78
77
  _: str = CONFIG_PARAM,
79
78
  ) -> None:
80
79
  """Add a new repository."""
@@ -86,15 +85,24 @@ async def add(
86
85
  "name": {"value": name},
87
86
  "location": {"value": location},
88
87
  "description": {"value": description},
89
- "commit": {"value": commit},
90
88
  },
91
89
  }
90
+ if read_only:
91
+ input_data["data"]["ref"] = {"value": ref}
92
+ else:
93
+ input_data["data"]["default_branch"] = {"value": ref}
92
94
 
93
95
  client = initialize_client()
94
96
 
95
- credential = await client.create(kind="CorePasswordCredential", name=name, username=username, password=password)
96
- await credential.save(allow_upsert=True)
97
- input_data["data"]["credential"] = {"id": credential.id}
97
+ if username or password:
98
+ credential = await client.create(
99
+ kind="CorePasswordCredential",
100
+ name=name,
101
+ username=username,
102
+ password=password,
103
+ )
104
+ await credential.save(allow_upsert=True)
105
+ input_data["data"]["credential"] = {"id": credential.id}
98
106
 
99
107
  query = Mutation(
100
108
  mutation="CoreReadOnlyRepositoryCreate" if read_only else "CoreRepositoryCreate",
@@ -102,18 +110,18 @@ async def add(
102
110
  query={"ok": None},
103
111
  )
104
112
 
105
- await client.execute_graphql(query=query.render(), branch_name=branch, tracker="mutation-repository-create")
113
+ await client.execute_graphql(query=query.render(), tracker="mutation-repository-create")
106
114
 
107
115
 
108
116
  @app.command()
109
117
  async def list(
110
- branch: str | None = None,
118
+ branch: Optional[str] = typer.Option(None, help="Branch on which to list repositories."),
111
119
  debug: bool = False,
112
120
  _: str = CONFIG_PARAM,
113
121
  ) -> None:
114
122
  init_logging(debug=debug)
115
123
 
116
- client = initialize_client(branch=branch)
124
+ client = initialize_client()
117
125
 
118
126
  repo_status_query = {
119
127
  "CoreGenericRepository": {
@@ -108,7 +108,7 @@ def get_node(schemas_data: list[dict], schema_index: int, node_index: int) -> di
108
108
  async def load(
109
109
  schemas: list[Path],
110
110
  debug: bool = False,
111
- branch: str = typer.Option("main", help="Branch on which to load the schema."),
111
+ branch: str = typer.Option(None, help="Branch on which to load the schema."),
112
112
  wait: int = typer.Option(0, help="Time in seconds to wait until the schema has converged across all workers"),
113
113
  _: str = CONFIG_PARAM,
114
114
  ) -> None:
@@ -159,7 +159,7 @@ async def load(
159
159
  async def check(
160
160
  schemas: list[Path],
161
161
  debug: bool = False,
162
- branch: str = typer.Option("main", help="Branch on which to check the schema."),
162
+ branch: str = typer.Option(None, help="Branch on which to check the schema."),
163
163
  _: str = CONFIG_PARAM,
164
164
  ) -> None:
165
165
  """Check if schema files are valid and what would be the impact of loading them with Infrahub."""
infrahub_sdk/ctl/utils.py CHANGED
@@ -6,7 +6,7 @@ import traceback
6
6
  from collections.abc import Coroutine
7
7
  from functools import wraps
8
8
  from pathlib import Path
9
- from typing import TYPE_CHECKING, Any, Callable, NoReturn, TypeVar
9
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn, Optional, TypeVar
10
10
 
11
11
  import pendulum
12
12
  import typer
@@ -17,10 +17,10 @@ from rich.console import Console
17
17
  from rich.logging import RichHandler
18
18
  from rich.markup import escape
19
19
 
20
- from ..ctl.exceptions import FileNotValidError, QueryNotFoundError
21
20
  from ..exceptions import (
22
21
  AuthenticationError,
23
22
  Error,
23
+ FileNotValidError,
24
24
  GraphQLError,
25
25
  NodeNotFoundError,
26
26
  ResourceNotDefinedError,
@@ -30,6 +30,7 @@ from ..exceptions import (
30
30
  )
31
31
  from ..yaml import YamlFile
32
32
  from .client import initialize_client_sync
33
+ from .exceptions import QueryNotFoundError
33
34
 
34
35
  if TYPE_CHECKING:
35
36
  from ..schema.repository import InfrahubRepositoryConfig
@@ -144,7 +145,7 @@ def print_graphql_errors(console: Console, errors: list) -> None:
144
145
  console.print(f"[red]{escape(str(error))}")
145
146
 
146
147
 
147
- def parse_cli_vars(variables: list[str] | None) -> dict[str, str]:
148
+ def parse_cli_vars(variables: Optional[list[str]]) -> dict[str, str]:
148
149
  if not variables:
149
150
  return {}
150
151
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import sys
4
4
  from pathlib import Path
5
+ from typing import Optional
5
6
 
6
7
  import typer
7
8
  import ujson
@@ -57,7 +58,7 @@ async def validate_schema(schema: Path, _: str = CONFIG_PARAM) -> None:
57
58
  @catch_exception(console=console)
58
59
  def validate_graphql(
59
60
  query: str,
60
- variables: list[str] | None = typer.Argument(
61
+ variables: Optional[list[str]] = typer.Argument(
61
62
  None, help="Variables to pass along with the query. Format key=value key=value."
62
63
  ),
63
64
  debug: bool = typer.Option(False, help="Display more troubleshooting information."),
@@ -121,6 +121,12 @@ class AuthenticationError(Error):
121
121
  super().__init__(self.message)
122
122
 
123
123
 
124
+ class URLNotFoundError(Error):
125
+ def __init__(self, url: str):
126
+ self.message = f"`{url}` not found."
127
+ super().__init__(self.message)
128
+
129
+
124
130
  class FeatureNotSupportedError(Error):
125
131
  """Raised when trying to use a method on a node that doesn't support it."""
126
132
 
@@ -131,3 +137,9 @@ class UninitializedError(Error):
131
137
 
132
138
  class InvalidResponseError(Error):
133
139
  """Raised when an object requires an initialization step before use"""
140
+
141
+
142
+ class FileNotValidError(Error):
143
+ def __init__(self, name: str, message: str = ""):
144
+ self.message = message or f"Cannot parse '{name}' content."
145
+ super().__init__(self.message)
infrahub_sdk/generator.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import os
4
5
  from abc import abstractmethod
5
6
  from typing import TYPE_CHECKING
@@ -27,6 +28,7 @@ class InfrahubGenerator:
27
28
  generator_instance: str = "",
28
29
  params: dict | None = None,
29
30
  convert_query_response: bool = False,
31
+ logger: logging.Logger | None = None,
30
32
  ) -> None:
31
33
  self.query = query
32
34
  self.branch = branch
@@ -41,6 +43,7 @@ class InfrahubGenerator:
41
43
  self._related_nodes: list[InfrahubNode] = []
42
44
  self.infrahub_node = infrahub_node
43
45
  self.convert_query_response = convert_query_response
46
+ self.logger = logger if logger else logging.getLogger("infrahub.tasks")
44
47
 
45
48
  @property
46
49
  def store(self) -> NodeStore:
infrahub_sdk/node.py CHANGED
@@ -187,6 +187,7 @@ class RelatedNodeBase:
187
187
  if node_data:
188
188
  self._id = node_data.get("id", None)
189
189
  self._hfid = node_data.get("hfid", None)
190
+ self._kind = node_data.get("kind", None)
190
191
  self._display_label = node_data.get("display_label", None)
191
192
  self._typename = node_data.get("__typename", None)
192
193
 
@@ -255,6 +256,8 @@ class RelatedNodeBase:
255
256
  data["id"] = self.id
256
257
  elif self.hfid is not None:
257
258
  data["hfid"] = self.hfid
259
+ if self._kind is not None:
260
+ data["kind"] = self._kind
258
261
 
259
262
  for prop_name in self._properties:
260
263
  if getattr(self, prop_name) is not None:
@@ -1118,14 +1121,14 @@ class InfrahubNode(InfrahubNodeBase):
1118
1121
  async def artifact_generate(self, name: str) -> None:
1119
1122
  self._validate_artifact_support(ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE)
1120
1123
 
1121
- artifact = await self._client.get(kind="CoreArtifact", definition__name__value=name, object__ids=[self.id])
1124
+ artifact = await self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
1122
1125
  await artifact.definition.fetch() # type: ignore[attr-defined]
1123
1126
  await artifact.definition.peer.generate([artifact.id]) # type: ignore[attr-defined]
1124
1127
 
1125
1128
  async def artifact_fetch(self, name: str) -> str | dict[str, Any]:
1126
1129
  self._validate_artifact_support(ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE)
1127
1130
 
1128
- artifact = await self._client.get(kind="CoreArtifact", definition__name__value=name, object__ids=[self.id])
1131
+ artifact = await self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
1129
1132
  content = await self._client.object_store.get(identifier=artifact.storage_id.value) # type: ignore[attr-defined]
1130
1133
  return content
1131
1134
 
@@ -1635,13 +1638,13 @@ class InfrahubNodeSync(InfrahubNodeBase):
1635
1638
 
1636
1639
  def artifact_generate(self, name: str) -> None:
1637
1640
  self._validate_artifact_support(ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE)
1638
- artifact = self._client.get(kind="CoreArtifact", definition__name__value=name, object__ids=[self.id])
1641
+ artifact = self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
1639
1642
  artifact.definition.fetch() # type: ignore[attr-defined]
1640
1643
  artifact.definition.peer.generate([artifact.id]) # type: ignore[attr-defined]
1641
1644
 
1642
1645
  def artifact_fetch(self, name: str) -> str | dict[str, Any]:
1643
1646
  self._validate_artifact_support(ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE)
1644
- artifact = self._client.get(kind="CoreArtifact", definition__name__value=name, object__ids=[self.id])
1647
+ artifact = self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
1645
1648
  content = self._client.object_store.get(identifier=artifact.storage_id.value) # type: ignore[attr-defined]
1646
1649
  return content
1647
1650
 
@@ -80,6 +80,7 @@ class SchemaAnimal:
80
80
  namespace=NAMESPACE,
81
81
  include_in_menu=True,
82
82
  inherit_from=[TESTING_ANIMAL],
83
+ human_friendly_id=["owner__name__value", "name__value", "color__value"],
83
84
  display_labels=["name__value", "breed__value", "color__value"],
84
85
  order_by=["name__value"],
85
86
  attributes=[
@@ -108,6 +109,14 @@ class SchemaAnimal:
108
109
  identifier="person__animal",
109
110
  cardinality="many",
110
111
  direction=RelationshipDirection.INBOUND,
112
+ max_count=10,
113
+ ),
114
+ Rel(
115
+ name="favorite_animal",
116
+ peer=TESTING_ANIMAL,
117
+ identifier="favorite_animal",
118
+ cardinality="one",
119
+ direction=RelationshipDirection.INBOUND,
111
120
  ),
112
121
  Rel(
113
122
  name="best_friends",
infrahub_sdk/utils.py CHANGED
@@ -17,7 +17,7 @@ from graphql import (
17
17
 
18
18
  from infrahub_sdk.repository import GitRepoManager
19
19
 
20
- from .exceptions import JsonDecodeError
20
+ from .exceptions import FileNotValidError, JsonDecodeError
21
21
 
22
22
  if TYPE_CHECKING:
23
23
  from graphql import GraphQLResolveInfo
@@ -342,6 +342,16 @@ def write_to_file(path: Path, value: Any) -> bool:
342
342
  return written is not None
343
343
 
344
344
 
345
+ def read_file(file_name: Path) -> str:
346
+ if not file_name.is_file():
347
+ raise FileNotValidError(name=str(file_name), message=f"{file_name} is not a valid file")
348
+ try:
349
+ with Path.open(file_name, encoding="utf-8") as fobj:
350
+ return fobj.read()
351
+ except UnicodeDecodeError as exc:
352
+ raise FileNotValidError(name=str(file_name), message=f"Unable to read {file_name} with utf-8 encoding") from exc
353
+
354
+
345
355
  def get_user_permissions(data: list[dict]) -> dict:
346
356
  groups = {}
347
357
  for group in data:
infrahub_sdk/yaml.py CHANGED
@@ -8,9 +8,8 @@ import yaml
8
8
  from pydantic import BaseModel, Field
9
9
  from typing_extensions import Self
10
10
 
11
- from .ctl._file import read_file
12
- from .ctl.exceptions import FileNotValidError
13
- from .utils import find_files
11
+ from .exceptions import FileNotValidError
12
+ from .utils import find_files, read_file
14
13
 
15
14
 
16
15
  class InfrahubFileApiVersion(str, Enum):