infrahub-server 1.4.10__py3-none-any.whl → 1.5.0b0__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 (103) hide show
  1. infrahub/actions/tasks.py +200 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/query.py +2 -0
  4. infrahub/api/schema.py +3 -0
  5. infrahub/auth.py +5 -5
  6. infrahub/cli/db.py +2 -2
  7. infrahub/config.py +7 -2
  8. infrahub/core/attribute.py +22 -19
  9. infrahub/core/branch/models.py +2 -2
  10. infrahub/core/branch/needs_rebase_status.py +11 -0
  11. infrahub/core/branch/tasks.py +2 -2
  12. infrahub/core/constants/__init__.py +1 -0
  13. infrahub/core/convert_object_type/object_conversion.py +201 -0
  14. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  15. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  16. infrahub/core/diff/query/artifact.py +1 -1
  17. infrahub/core/graph/__init__.py +1 -1
  18. infrahub/core/initialization.py +2 -2
  19. infrahub/core/manager.py +3 -81
  20. infrahub/core/migrations/graph/__init__.py +2 -0
  21. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
  22. infrahub/core/node/__init__.py +23 -2
  23. infrahub/core/node/create.py +67 -35
  24. infrahub/core/node/lock_utils.py +98 -0
  25. infrahub/core/property.py +11 -0
  26. infrahub/core/protocols.py +1 -0
  27. infrahub/core/query/attribute.py +27 -15
  28. infrahub/core/query/node.py +47 -184
  29. infrahub/core/query/relationship.py +43 -26
  30. infrahub/core/query/subquery.py +0 -8
  31. infrahub/core/relationship/model.py +59 -19
  32. infrahub/core/schema/attribute_schema.py +0 -2
  33. infrahub/core/schema/definitions/core/repository.py +7 -0
  34. infrahub/core/schema/relationship_schema.py +0 -1
  35. infrahub/core/schema/schema_branch.py +3 -2
  36. infrahub/generators/models.py +31 -12
  37. infrahub/generators/tasks.py +3 -1
  38. infrahub/git/base.py +38 -1
  39. infrahub/graphql/api/dependencies.py +2 -4
  40. infrahub/graphql/api/endpoints.py +2 -2
  41. infrahub/graphql/app.py +2 -4
  42. infrahub/graphql/initialization.py +2 -3
  43. infrahub/graphql/manager.py +212 -137
  44. infrahub/graphql/middleware.py +12 -0
  45. infrahub/graphql/mutations/branch.py +11 -0
  46. infrahub/graphql/mutations/computed_attribute.py +110 -3
  47. infrahub/graphql/mutations/convert_object_type.py +34 -13
  48. infrahub/graphql/mutations/ipam.py +21 -8
  49. infrahub/graphql/mutations/main.py +37 -153
  50. infrahub/graphql/mutations/profile.py +195 -0
  51. infrahub/graphql/mutations/proposed_change.py +2 -1
  52. infrahub/graphql/mutations/repository.py +22 -83
  53. infrahub/graphql/mutations/webhook.py +1 -1
  54. infrahub/graphql/registry.py +173 -0
  55. infrahub/graphql/schema.py +4 -1
  56. infrahub/lock.py +52 -26
  57. infrahub/locks/__init__.py +0 -0
  58. infrahub/locks/tasks.py +37 -0
  59. infrahub/patch/plan_writer.py +2 -2
  60. infrahub/profiles/__init__.py +0 -0
  61. infrahub/profiles/node_applier.py +101 -0
  62. infrahub/profiles/queries/__init__.py +0 -0
  63. infrahub/profiles/queries/get_profile_data.py +99 -0
  64. infrahub/profiles/tasks.py +63 -0
  65. infrahub/repositories/__init__.py +0 -0
  66. infrahub/repositories/create_repository.py +113 -0
  67. infrahub/tasks/registry.py +6 -4
  68. infrahub/webhook/models.py +1 -1
  69. infrahub/workflows/catalogue.py +38 -3
  70. infrahub/workflows/models.py +17 -2
  71. infrahub_sdk/branch.py +5 -8
  72. infrahub_sdk/client.py +364 -84
  73. infrahub_sdk/convert_object_type.py +61 -0
  74. infrahub_sdk/ctl/check.py +2 -3
  75. infrahub_sdk/ctl/cli_commands.py +16 -12
  76. infrahub_sdk/ctl/config.py +8 -2
  77. infrahub_sdk/ctl/generator.py +2 -3
  78. infrahub_sdk/ctl/repository.py +39 -1
  79. infrahub_sdk/ctl/schema.py +12 -1
  80. infrahub_sdk/ctl/utils.py +4 -0
  81. infrahub_sdk/ctl/validate.py +5 -3
  82. infrahub_sdk/diff.py +4 -5
  83. infrahub_sdk/exceptions.py +2 -0
  84. infrahub_sdk/graphql.py +7 -2
  85. infrahub_sdk/node/attribute.py +2 -0
  86. infrahub_sdk/node/node.py +28 -20
  87. infrahub_sdk/playback.py +1 -2
  88. infrahub_sdk/protocols.py +40 -6
  89. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  90. infrahub_sdk/pytest_plugin/utils.py +40 -0
  91. infrahub_sdk/repository.py +1 -2
  92. infrahub_sdk/schema/main.py +1 -0
  93. infrahub_sdk/spec/object.py +43 -4
  94. infrahub_sdk/spec/range_expansion.py +118 -0
  95. infrahub_sdk/timestamp.py +18 -6
  96. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b0.dist-info}/METADATA +6 -9
  97. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b0.dist-info}/RECORD +102 -84
  98. infrahub_testcontainers/models.py +2 -2
  99. infrahub_testcontainers/performance_test.py +4 -4
  100. infrahub/core/convert_object_type/conversion.py +0 -134
  101. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b0.dist-info}/LICENSE.txt +0 -0
  102. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b0.dist-info}/WHEEL +0 -0
  103. {infrahub_server-1.4.10.dist-info → infrahub_server-1.5.0b0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, model_validator
6
+
7
+ CONVERT_OBJECT_MUTATION = """
8
+ mutation($node_id: String!, $target_kind: String!, $fields_mapping: GenericScalar!) {
9
+ ConvertObjectType(data: {
10
+ node_id: $node_id,
11
+ target_kind: $target_kind,
12
+ fields_mapping: $fields_mapping
13
+ }) {
14
+ ok
15
+ node
16
+ }
17
+ }
18
+ """
19
+
20
+
21
+ class ConversionFieldValue(BaseModel): # Only one of these fields can be not None
22
+ """
23
+ Holds the new value of the destination field during an object conversion.
24
+ Use `attribute_value` to specify the new raw value of an attribute.
25
+ Use `peer_id` to specify new peer of a cardinality one relationship.
26
+ Use `peers_ids` to specify new peers of a cardinality many relationship.
27
+ Only one of `attribute_value`, `peer_id` and `peers_ids` can be specified.
28
+ """
29
+
30
+ attribute_value: Any | None = None
31
+ peer_id: str | None = None
32
+ peers_ids: list[str] | None = None
33
+
34
+ @model_validator(mode="after")
35
+ def check_only_one_field(self) -> ConversionFieldValue:
36
+ fields = [self.attribute_value, self.peer_id, self.peers_ids]
37
+ set_fields = [f for f in fields if f is not None]
38
+ if len(set_fields) != 1:
39
+ raise ValueError("Exactly one of `attribute_value`, `peer_id`, or `peers_ids` must be set")
40
+ return self
41
+
42
+
43
+ class ConversionFieldInput(BaseModel):
44
+ """
45
+ Indicates how to fill in the value of the destination field during an object conversion.
46
+ Use `source_field` to reuse the value of the corresponding field of the object being converted.
47
+ Use `data` to specify the new value for the field.
48
+ Use `use_default_value` to set the destination field to its schema default.
49
+ Only one of `source_field`, `data`, or `use_default_value` can be specified.
50
+ """
51
+
52
+ source_field: str | None = None
53
+ data: ConversionFieldValue | None = None
54
+ use_default_value: bool = False
55
+
56
+ @model_validator(mode="after")
57
+ def check_only_one_field(self) -> ConversionFieldInput:
58
+ fields_set = [self.source_field is not None, self.data is not None, self.use_default_value is True]
59
+ if sum(fields_set) != 1:
60
+ raise ValueError("Exactly one of `source_field`, `data` or `use_default_value` must be set")
61
+ return self
infrahub_sdk/ctl/check.py CHANGED
@@ -11,10 +11,9 @@ import typer
11
11
  from rich.console import Console
12
12
  from rich.logging import RichHandler
13
13
 
14
- from ..ctl import config
15
14
  from ..ctl.client import initialize_client
16
15
  from ..ctl.exceptions import QueryNotFoundError
17
- from ..ctl.repository import get_repository_config
16
+ from ..ctl.repository import find_repository_config_file, get_repository_config
18
17
  from ..ctl.utils import catch_exception, execute_graphql_query
19
18
  from ..exceptions import ModuleImportError
20
19
 
@@ -59,7 +58,7 @@ def run(
59
58
  FORMAT = "%(message)s"
60
59
  logging.basicConfig(level=log_level, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()])
61
60
 
62
- repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE))
61
+ repository_config = get_repository_config(find_repository_config_file())
63
62
 
64
63
  if list_available:
65
64
  list_checks(repository_config=repository_config)
@@ -20,7 +20,6 @@ from rich.table import Table
20
20
 
21
21
  from .. import __version__ as sdk_version
22
22
  from ..async_typer import AsyncTyper
23
- from ..ctl import config
24
23
  from ..ctl.branch import app as branch_app
25
24
  from ..ctl.check import run as run_check
26
25
  from ..ctl.client import initialize_client, initialize_client_sync
@@ -30,7 +29,7 @@ from ..ctl.menu import app as menu_app
30
29
  from ..ctl.object import app as object_app
31
30
  from ..ctl.render import list_jinja2_transforms, print_template_errors
32
31
  from ..ctl.repository import app as repository_app
33
- from ..ctl.repository import get_repository_config
32
+ from ..ctl.repository import find_repository_config_file, get_repository_config
34
33
  from ..ctl.schema import app as schema_app
35
34
  from ..ctl.transform import list_transforms
36
35
  from ..ctl.utils import (
@@ -46,7 +45,7 @@ from ..protocols_generator.generator import CodeGenerator
46
45
  from ..schema import MainSchemaTypesAll, SchemaRoot
47
46
  from ..template import Jinja2Template
48
47
  from ..template.exceptions import JinjaTemplateError
49
- from ..utils import get_branch, write_to_file
48
+ from ..utils import write_to_file
50
49
  from ..yaml import SchemaFile
51
50
  from .exporter import dump
52
51
  from .importer import load
@@ -208,7 +207,6 @@ async def _run_transform(
208
207
  debug: Prints debug info to the command line
209
208
  repository_config: Repository config object. This is used to load the graphql query from the repository.
210
209
  """
211
- branch = get_branch(branch)
212
210
 
213
211
  try:
214
212
  response = execute_graphql_query(
@@ -260,7 +258,7 @@ async def render(
260
258
  """Render a local Jinja2 Transform for debugging purpose."""
261
259
 
262
260
  variables_dict = parse_cli_vars(variables)
263
- repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE))
261
+ repository_config = get_repository_config(find_repository_config_file())
264
262
 
265
263
  if list_available or not transform_name:
266
264
  list_jinja2_transforms(config=repository_config)
@@ -270,7 +268,7 @@ async def render(
270
268
  try:
271
269
  transform_config = repository_config.get_jinja2_transform(name=transform_name)
272
270
  except KeyError as exc:
273
- console.print(f'[red]Unable to find "{transform_name}" in {config.INFRAHUB_REPO_CONFIG_FILE}')
271
+ console.print(f'[red]Unable to find "{transform_name}" in repository config file')
274
272
  list_jinja2_transforms(config=repository_config)
275
273
  raise typer.Exit(1) from exc
276
274
 
@@ -310,7 +308,7 @@ def transform(
310
308
  """Render a local transform (TransformPython) for debugging purpose."""
311
309
 
312
310
  variables_dict = parse_cli_vars(variables)
313
- repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE))
311
+ repository_config = get_repository_config(find_repository_config_file())
314
312
 
315
313
  if list_available or not transform_name:
316
314
  list_transforms(config=repository_config)
@@ -409,7 +407,6 @@ def info( # noqa: PLR0915
409
407
  _: str = CONFIG_PARAM,
410
408
  ) -> None:
411
409
  """Display the status of the Python SDK."""
412
-
413
410
  info: dict[str, Any] = {
414
411
  "error": None,
415
412
  "status": ":x:",
@@ -417,12 +414,17 @@ def info( # noqa: PLR0915
417
414
  "user_info": {},
418
415
  "groups": {},
419
416
  }
417
+ client = initialize_client_sync()
418
+ fetch_user_details = bool(client.config.username) or bool(client.config.api_token)
419
+
420
420
  try:
421
- client = initialize_client_sync()
422
421
  info["infrahub_version"] = client.get_version()
423
- info["user_info"] = client.get_user()
422
+
423
+ if fetch_user_details:
424
+ info["user_info"] = client.get_user()
425
+ info["groups"] = client.get_user_permissions()
426
+
424
427
  info["status"] = ":white_heavy_check_mark:"
425
- info["groups"] = client.get_user_permissions()
426
428
  except Exception as e:
427
429
  info["error"] = f"{e!s} ({e.__class__.__name__})"
428
430
 
@@ -469,7 +471,7 @@ def info( # noqa: PLR0915
469
471
  pretty_model = Pretty(client.config.model_dump(), expand_all=True)
470
472
  layout["client_info"].update(Panel(pretty_model, title="Client Info"))
471
473
 
472
- # Infrahub information planel
474
+ # Infrahub information panel
473
475
  infrahub_info = Table(show_header=False, box=None)
474
476
  if info["user_info"]:
475
477
  infrahub_info.add_row("User:", info["user_info"]["AccountProfile"]["display_label"])
@@ -487,6 +489,8 @@ def info( # noqa: PLR0915
487
489
  infrahub_info.add_row("Groups:", "")
488
490
  for group, roles in groups.items():
489
491
  infrahub_info.add_row("", group, ", ".join(roles))
492
+ else:
493
+ infrahub_info.add_row("User:", "anonymous")
490
494
 
491
495
  layout["infrahub_info"].update(Panel(infrahub_info, title="Infrahub Info"))
492
496
 
@@ -2,16 +2,22 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import sys
5
6
  from pathlib import Path
6
7
 
7
- import toml
8
8
  import typer
9
9
  from pydantic import Field, ValidationError, field_validator
10
10
  from pydantic_settings import BaseSettings, SettingsConfigDict
11
11
 
12
+ if sys.version_info >= (3, 11):
13
+ import tomllib
14
+ else:
15
+ import tomli as tomllib
16
+
12
17
  DEFAULT_CONFIG_FILE = "infrahubctl.toml"
13
18
  ENVVAR_CONFIG_FILE = "INFRAHUBCTL_CONFIG"
14
19
  INFRAHUB_REPO_CONFIG_FILE = ".infrahub.yml"
20
+ INFRAHUB_REPO_CONFIG_FILE_ALT = ".infrahub.yaml"
15
21
 
16
22
 
17
23
  class Settings(BaseSettings):
@@ -59,7 +65,7 @@ class ConfiguredSettings:
59
65
 
60
66
  if config_file.is_file():
61
67
  config_string = config_file.read_text(encoding="utf-8")
62
- config_tmp = toml.loads(config_string)
68
+ config_tmp = tomllib.loads(config_string)
63
69
 
64
70
  self._settings = Settings(**config_tmp)
65
71
  return
@@ -6,9 +6,8 @@ from typing import TYPE_CHECKING, Optional
6
6
  import typer
7
7
  from rich.console import Console
8
8
 
9
- from ..ctl import config
10
9
  from ..ctl.client import initialize_client
11
- from ..ctl.repository import get_repository_config
10
+ from ..ctl.repository import find_repository_config_file, get_repository_config
12
11
  from ..ctl.utils import execute_graphql_query, init_logging, parse_cli_vars
13
12
  from ..exceptions import ModuleImportError
14
13
  from ..node import InfrahubNode
@@ -26,7 +25,7 @@ async def run(
26
25
  variables: Optional[list[str]] = None,
27
26
  ) -> None:
28
27
  init_logging(debug=debug)
29
- repository_config = get_repository_config(Path(config.INFRAHUB_REPO_CONFIG_FILE))
28
+ repository_config = get_repository_config(find_repository_config_file())
30
29
 
31
30
  if list_available or not generator_name:
32
31
  list_generators(repository_config=repository_config)
@@ -24,11 +24,49 @@ app = AsyncTyper()
24
24
  console = Console()
25
25
 
26
26
 
27
+ def find_repository_config_file(base_path: Path | None = None) -> Path:
28
+ """Find the repository config file, checking for both .yml and .yaml extensions.
29
+
30
+ Args:
31
+ base_path: Base directory to search in. If None, uses current directory.
32
+
33
+ Returns:
34
+ Path to the config file.
35
+
36
+ Raises:
37
+ FileNotFoundError: If neither .infrahub.yml nor .infrahub.yaml exists.
38
+ """
39
+ if base_path is None:
40
+ base_path = Path()
41
+
42
+ yml_path = base_path / ".infrahub.yml"
43
+ yaml_path = base_path / ".infrahub.yaml"
44
+
45
+ # Prefer .yml if both exist
46
+ if yml_path.exists():
47
+ return yml_path
48
+ if yaml_path.exists():
49
+ return yaml_path
50
+ # For backward compatibility, return .yml path for error messages
51
+ return yml_path
52
+
53
+
27
54
  def get_repository_config(repo_config_file: Path) -> InfrahubRepositoryConfig:
55
+ # If the file doesn't exist, try to find it with alternate extension
56
+ if not repo_config_file.exists():
57
+ if repo_config_file.name == ".infrahub.yml":
58
+ alt_path = repo_config_file.parent / ".infrahub.yaml"
59
+ if alt_path.exists():
60
+ repo_config_file = alt_path
61
+ elif repo_config_file.name == ".infrahub.yaml":
62
+ alt_path = repo_config_file.parent / ".infrahub.yml"
63
+ if alt_path.exists():
64
+ repo_config_file = alt_path
65
+
28
66
  try:
29
67
  config_file_data = load_repository_config_file(repo_config_file)
30
68
  except FileNotFoundError as exc:
31
- console.print(f"[red]File not found {exc}")
69
+ console.print(f"[red]File not found {exc} (also checked for .infrahub.yml and .infrahub.yaml)")
32
70
  raise typer.Exit(1) from exc
33
71
  except FileNotValidError as exc:
34
72
  console.print(f"[red]{exc.message}")
@@ -77,7 +77,18 @@ def display_schema_load_errors(response: dict[str, Any], schemas_data: list[Sche
77
77
 
78
78
  elif len(loc_path) > 6:
79
79
  loc_type = loc_path[5]
80
- input_label = node[loc_type][loc_path[6]].get("name", None)
80
+ error_data = node[loc_type]
81
+ attribute = loc_path[6]
82
+
83
+ if isinstance(attribute, str):
84
+ input_label = None
85
+ for data in error_data:
86
+ if data.get(attribute) is not None:
87
+ input_label = data.get("name", None)
88
+ break
89
+ else:
90
+ input_label = error_data[attribute].get("name", None)
91
+
81
92
  input_str = error.get("input", None)
82
93
  error_message = f"{loc_type[:-1].title()}: {input_label} ({input_str}) | {error['msg']} ({error['type']})"
83
94
  console.print(f" Node: {node.get('namespace', None)}{node.get('name', None)} | {error_message}")
infrahub_sdk/ctl/utils.py CHANGED
@@ -118,6 +118,10 @@ def execute_graphql_query(
118
118
  query_str = query_object.load_query()
119
119
 
120
120
  client = initialize_client_sync()
121
+
122
+ if not branch:
123
+ branch = client.config.default_infrahub_branch
124
+
121
125
  response = client.execute_graphql(
122
126
  query=query_str,
123
127
  branch_name=branch,
@@ -14,7 +14,7 @@ from ..ctl.client import initialize_client, initialize_client_sync
14
14
  from ..ctl.exceptions import QueryNotFoundError
15
15
  from ..ctl.utils import catch_exception, find_graphql_query, parse_cli_vars
16
16
  from ..exceptions import GraphQLError
17
- from ..utils import get_branch, write_to_file
17
+ from ..utils import write_to_file
18
18
  from ..yaml import SchemaFile
19
19
  from .parameters import CONFIG_PARAM
20
20
  from .utils import load_yamlfile_from_disk_and_exit
@@ -68,8 +68,6 @@ def validate_graphql(
68
68
  ) -> None:
69
69
  """Validate the format of a GraphQL Query stored locally by executing it on a remote GraphQL endpoint"""
70
70
 
71
- branch = get_branch(branch)
72
-
73
71
  try:
74
72
  query_str = find_graphql_query(query)
75
73
  except QueryNotFoundError:
@@ -81,6 +79,10 @@ def validate_graphql(
81
79
  variables_dict = parse_cli_vars(variables)
82
80
 
83
81
  client = initialize_client_sync()
82
+
83
+ if not branch:
84
+ branch = client.config.default_infrahub_branch
85
+
84
86
  try:
85
87
  response = client.execute_graphql(
86
88
  query=query_str,
infrahub_sdk/diff.py CHANGED
@@ -37,8 +37,8 @@ class NodeDiffPeer(TypedDict):
37
37
 
38
38
  def get_diff_summary_query() -> str:
39
39
  return """
40
- query GetDiffTree($branch_name: String!) {
41
- DiffTree(branch: $branch_name) {
40
+ query GetDiffTree($branch_name: String!, $name: String, $from_time: DateTime, $to_time: DateTime) {
41
+ DiffTree(branch: $branch_name, name: $name, from_time: $from_time, to_time: $to_time) {
42
42
  nodes {
43
43
  uuid
44
44
  kind
@@ -117,12 +117,11 @@ def diff_tree_node_to_node_diff(node_dict: dict[str, Any], branch_name: str) ->
117
117
  )
118
118
  relationship_diff["peers"] = peer_diffs
119
119
  element_diffs.append(relationship_diff)
120
- node_diff = NodeDiff(
120
+ return NodeDiff(
121
121
  branch=branch_name,
122
122
  kind=str(node_dict.get("kind")),
123
123
  id=str(node_dict.get("uuid")),
124
- action=str(node_dict.get("action")),
124
+ action=str(node_dict.get("status")),
125
125
  display_label=str(node_dict.get("label")),
126
126
  elements=element_diffs,
127
127
  )
128
- return node_diff
@@ -17,6 +17,8 @@ class JsonDecodeError(Error):
17
17
  self.url = url
18
18
  if not self.message and self.url:
19
19
  self.message = f"Unable to decode response as JSON data from {self.url}"
20
+ if self.content:
21
+ self.message += f". Server response: {self.content}"
20
22
  super().__init__(self.message)
21
23
 
22
24
 
infrahub_sdk/graphql.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from enum import Enum
4
5
  from typing import Any
5
6
 
@@ -8,7 +9,9 @@ from pydantic import BaseModel
8
9
  VARIABLE_TYPE_MAPPING = ((str, "String!"), (int, "Int!"), (float, "Float!"), (bool, "Boolean!"))
9
10
 
10
11
 
11
- def convert_to_graphql_as_string(value: str | bool | list | BaseModel | Enum | Any, convert_enum: bool = False) -> str: # noqa: PLR0911
12
+ def convert_to_graphql_as_string(value: Any, convert_enum: bool = False) -> str: # noqa: PLR0911
13
+ if value is None:
14
+ return "null"
12
15
  if isinstance(value, str) and value.startswith("$"):
13
16
  return value
14
17
  if isinstance(value, Enum):
@@ -16,7 +19,9 @@ def convert_to_graphql_as_string(value: str | bool | list | BaseModel | Enum | A
16
19
  return convert_to_graphql_as_string(value=value.value, convert_enum=True)
17
20
  return value.name
18
21
  if isinstance(value, str):
19
- return f'"{value}"'
22
+ # Use json.dumps() to properly escape the string according to JSON rules,
23
+ # which are compatible with GraphQL string escaping
24
+ return json.dumps(value)
20
25
  if isinstance(value, bool):
21
26
  return repr(value).lower()
22
27
  if isinstance(value, list):
@@ -76,6 +76,8 @@ class Attribute:
76
76
  variables: dict[str, Any] = {}
77
77
 
78
78
  if self.value is None:
79
+ if self._schema.optional and self.value_has_been_mutated:
80
+ data["value"] = None
79
81
  return data
80
82
 
81
83
  if isinstance(self.value, str):
infrahub_sdk/node/node.py CHANGED
@@ -234,15 +234,10 @@ class InfrahubNodeBase:
234
234
 
235
235
  rel: RelatedNodeBase | RelationshipManagerBase = getattr(self, item_name)
236
236
 
237
- # BLOCKED by https://github.com/opsmill/infrahub/issues/330
238
- # if (
239
- # item is None
240
- # and item_name in self._relationships
241
- # and self._schema.get_relationship(item_name).cardinality == "one"
242
- # ):
243
- # data[item_name] = None
244
- # continue
245
- # el
237
+ if rel_schema.cardinality == RelationshipCardinality.ONE and rel_schema.optional and not rel.initialized:
238
+ data[item_name] = None
239
+ continue
240
+
246
241
  if rel is None or not rel.initialized:
247
242
  continue
248
243
 
@@ -315,7 +310,16 @@ class InfrahubNodeBase:
315
310
  variables.pop(variable_key)
316
311
 
317
312
  # TODO: I do not feel _great_ about this
318
- if not data_item and data_item != [] and item in data:
313
+ # -> I don't even know who you are (but this is not great indeed) -- gmazoyer (quoting Thanos)
314
+ original_data_item = original_data.get(item)
315
+ original_data_item_is_none = original_data_item is None
316
+ if isinstance(original_data_item, dict):
317
+ if "node" in original_data_item:
318
+ original_data_item_is_none = original_data_item["node"] is None
319
+ elif "id" not in original_data_item:
320
+ original_data_item_is_none = True
321
+
322
+ if item in data and (data_item in ({}, []) or (data_item is None and original_data_item_is_none)):
319
323
  data.pop(item)
320
324
 
321
325
  def _strip_unmodified(self, data: dict, variables: dict) -> tuple[dict, dict]:
@@ -324,7 +328,9 @@ class InfrahubNodeBase:
324
328
  relationship_property = getattr(self, relationship)
325
329
  if not relationship_property or relationship not in data:
326
330
  continue
327
- if not relationship_property.initialized:
331
+ if not relationship_property.initialized and (
332
+ not isinstance(relationship_property, RelatedNodeBase) or not relationship_property.schema.optional
333
+ ):
328
334
  data.pop(relationship)
329
335
  elif isinstance(relationship_property, RelationshipManagerBase) and not relationship_property.has_update:
330
336
  data.pop(relationship)
@@ -573,8 +579,7 @@ class InfrahubNode(InfrahubNodeBase):
573
579
  self._validate_artifact_support(ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE)
574
580
 
575
581
  artifact = await self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
576
- content = await self._client.object_store.get(identifier=artifact._get_attribute(name="storage_id").value)
577
- return content
582
+ return await self._client.object_store.get(identifier=artifact._get_attribute(name="storage_id").value)
578
583
 
579
584
  async def delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None:
580
585
  input_data = {"data": {"id": self.id}}
@@ -742,12 +747,11 @@ class InfrahubNode(InfrahubNodeBase):
742
747
  continue
743
748
 
744
749
  peer_data: dict[str, Any] = {}
745
- if rel_schema and prefetch_relationships:
750
+ should_fetch_relationship = prefetch_relationships or (include is not None and rel_name in include)
751
+ if rel_schema and should_fetch_relationship:
746
752
  peer_schema = await self._client.schema.get(kind=rel_schema.peer, branch=self._branch)
747
753
  peer_node = InfrahubNode(client=self._client, schema=peer_schema, branch=self._branch)
748
754
  peer_data = await peer_node.generate_query_data_node(
749
- include=include,
750
- exclude=exclude,
751
755
  property=property,
752
756
  )
753
757
 
@@ -886,7 +890,11 @@ class InfrahubNode(InfrahubNodeBase):
886
890
  await self._process_mutation_result(mutation_name=mutation_name, response=response, timeout=timeout)
887
891
 
888
892
  async def _process_relationships(
889
- self, node_data: dict[str, Any], branch: str, related_nodes: list[InfrahubNode], timeout: int | None = None
893
+ self,
894
+ node_data: dict[str, Any],
895
+ branch: str,
896
+ related_nodes: list[InfrahubNode],
897
+ timeout: int | None = None,
890
898
  ) -> None:
891
899
  """Processes the Relationships of a InfrahubNode and add Related Nodes to a list.
892
900
 
@@ -1199,8 +1207,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
1199
1207
  def artifact_fetch(self, name: str) -> str | dict[str, Any]:
1200
1208
  self._validate_artifact_support(ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE)
1201
1209
  artifact = self._client.get(kind="CoreArtifact", name__value=name, object__ids=[self.id])
1202
- content = self._client.object_store.get(identifier=artifact._get_attribute(name="storage_id").value)
1203
- return content
1210
+ return self._client.object_store.get(identifier=artifact._get_attribute(name="storage_id").value)
1204
1211
 
1205
1212
  def delete(self, timeout: int | None = None, request_context: RequestContext | None = None) -> None:
1206
1213
  input_data = {"data": {"id": self.id}}
@@ -1363,7 +1370,8 @@ class InfrahubNodeSync(InfrahubNodeBase):
1363
1370
  continue
1364
1371
 
1365
1372
  peer_data: dict[str, Any] = {}
1366
- if rel_schema and prefetch_relationships:
1373
+ should_fetch_relationship = prefetch_relationships or (include is not None and rel_name in include)
1374
+ if rel_schema and should_fetch_relationship:
1367
1375
  peer_schema = self._client.schema.get(kind=rel_schema.peer, branch=self._branch)
1368
1376
  peer_node = InfrahubNodeSync(client=self._client, schema=peer_schema, branch=self._branch)
1369
1377
  peer_data = peer_node.generate_query_data_node(include=include, exclude=exclude, property=property)
infrahub_sdk/playback.py CHANGED
@@ -56,5 +56,4 @@ class JSONPlayback(BaseSettings):
56
56
  with Path(f"{self.directory}/{filename}.json").open(encoding="utf-8") as fobj:
57
57
  data = ujson.load(fobj)
58
58
 
59
- response = httpx.Response(status_code=data["status_code"], content=data["response_content"], request=request)
60
- return response
59
+ return httpx.Response(status_code=data["status_code"], content=data["response_content"], request=request)