infrahub-server 1.2.2__py3-none-any.whl → 1.2.4__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 (65) hide show
  1. infrahub/cli/git_agent.py +4 -10
  2. infrahub/computed_attribute/tasks.py +8 -8
  3. infrahub/config.py +35 -0
  4. infrahub/core/constants/__init__.py +1 -0
  5. infrahub/core/constraint/node/runner.py +6 -5
  6. infrahub/core/graph/__init__.py +1 -1
  7. infrahub/core/migrations/graph/__init__.py +6 -1
  8. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +68 -70
  9. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +69 -0
  10. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +26 -0
  11. infrahub/core/migrations/schema/node_attribute_remove.py +16 -2
  12. infrahub/core/models.py +7 -1
  13. infrahub/core/node/__init__.py +4 -4
  14. infrahub/core/node/constraints/grouped_uniqueness.py +30 -10
  15. infrahub/core/query/ipam.py +1 -1
  16. infrahub/core/registry.py +18 -0
  17. infrahub/core/schema/basenode_schema.py +21 -1
  18. infrahub/core/schema/definitions/internal.py +2 -1
  19. infrahub/core/schema/generated/base_node_schema.py +1 -1
  20. infrahub/core/schema/manager.py +21 -0
  21. infrahub/core/schema/schema_branch.py +17 -9
  22. infrahub/database/__init__.py +10 -0
  23. infrahub/events/group_action.py +6 -1
  24. infrahub/events/node_action.py +5 -1
  25. infrahub/git/integrator.py +9 -7
  26. infrahub/graphql/mutations/main.py +10 -12
  27. infrahub/menu/repository.py +6 -6
  28. infrahub/message_bus/messages/__init__.py +0 -2
  29. infrahub/message_bus/operations/__init__.py +0 -1
  30. infrahub/message_bus/operations/event/__init__.py +2 -2
  31. infrahub/server.py +6 -11
  32. infrahub/services/adapters/cache/__init__.py +17 -0
  33. infrahub/services/adapters/cache/redis.py +11 -1
  34. infrahub/services/adapters/message_bus/__init__.py +20 -0
  35. infrahub/services/component.py +1 -2
  36. infrahub/tasks/registry.py +3 -7
  37. infrahub/workers/infrahub_async.py +4 -10
  38. infrahub_sdk/client.py +6 -6
  39. infrahub_sdk/ctl/cli_commands.py +32 -37
  40. infrahub_sdk/ctl/render.py +39 -0
  41. infrahub_sdk/exceptions.py +6 -2
  42. infrahub_sdk/generator.py +1 -1
  43. infrahub_sdk/node.py +38 -11
  44. infrahub_sdk/protocols_base.py +8 -1
  45. infrahub_sdk/pytest_plugin/items/jinja2_transform.py +22 -26
  46. infrahub_sdk/schema/__init__.py +10 -1
  47. infrahub_sdk/store.py +351 -75
  48. infrahub_sdk/template/__init__.py +209 -0
  49. infrahub_sdk/template/exceptions.py +38 -0
  50. infrahub_sdk/template/filters.py +151 -0
  51. infrahub_sdk/template/models.py +10 -0
  52. infrahub_sdk/utils.py +7 -0
  53. {infrahub_server-1.2.2.dist-info → infrahub_server-1.2.4.dist-info}/METADATA +2 -1
  54. {infrahub_server-1.2.2.dist-info → infrahub_server-1.2.4.dist-info}/RECORD +61 -59
  55. infrahub_testcontainers/container.py +6 -0
  56. infrahub_testcontainers/docker-compose.test.yml +1 -0
  57. infrahub_testcontainers/haproxy.cfg +3 -3
  58. infrahub_testcontainers/helpers.py +1 -1
  59. infrahub/message_bus/messages/event_worker_newprimaryapi.py +0 -9
  60. infrahub/message_bus/operations/event/worker.py +0 -9
  61. infrahub/support/__init__.py +0 -0
  62. infrahub/support/macro.py +0 -69
  63. {infrahub_server-1.2.2.dist-info → infrahub_server-1.2.4.dist-info}/LICENSE.txt +0 -0
  64. {infrahub_server-1.2.2.dist-info → infrahub_server-1.2.4.dist-info}/WHEEL +0 -0
  65. {infrahub_server-1.2.2.dist-info → infrahub_server-1.2.4.dist-info}/entry_points.txt +0 -0
@@ -9,7 +9,6 @@ import sys
9
9
  from pathlib import Path
10
10
  from typing import TYPE_CHECKING, Any, Callable, Optional
11
11
 
12
- import jinja2
13
12
  import typer
14
13
  import ujson
15
14
  from rich.console import Console
@@ -18,7 +17,6 @@ from rich.logging import RichHandler
18
17
  from rich.panel import Panel
19
18
  from rich.pretty import Pretty
20
19
  from rich.table import Table
21
- from rich.traceback import Traceback
22
20
 
23
21
  from .. import __version__ as sdk_version
24
22
  from ..async_typer import AsyncTyper
@@ -31,7 +29,7 @@ from ..ctl.exceptions import QueryNotFoundError
31
29
  from ..ctl.generator import run as run_generator
32
30
  from ..ctl.menu import app as menu_app
33
31
  from ..ctl.object import app as object_app
34
- from ..ctl.render import list_jinja2_transforms
32
+ from ..ctl.render import list_jinja2_transforms, print_template_errors
35
33
  from ..ctl.repository import app as repository_app
36
34
  from ..ctl.repository import get_repository_config
37
35
  from ..ctl.schema import app as schema_app
@@ -44,8 +42,9 @@ from ..ctl.utils import (
44
42
  )
45
43
  from ..ctl.validate import app as validate_app
46
44
  from ..exceptions import GraphQLError, ModuleImportError
47
- from ..jinja2 import identify_faulty_jinja_code
48
45
  from ..schema import MainSchemaTypesAll, SchemaRoot
46
+ from ..template import Jinja2Template
47
+ from ..template.exceptions import JinjaTemplateError
49
48
  from ..utils import get_branch, write_to_file
50
49
  from ..yaml import SchemaFile
51
50
  from .exporter import dump
@@ -168,43 +167,28 @@ async def run(
168
167
  raise typer.Abort(f"Unable to Load the method {method} in the Python script at {script}")
169
168
 
170
169
  client = initialize_client(
171
- branch=branch, timeout=timeout, max_concurrent_execution=concurrent, identifier=module_name
170
+ branch=branch,
171
+ timeout=timeout,
172
+ max_concurrent_execution=concurrent,
173
+ identifier=module_name,
172
174
  )
173
175
  func = getattr(module, method)
174
176
  await func(client=client, log=log, branch=branch, **variables_dict)
175
177
 
176
178
 
177
- def render_jinja2_template(template_path: Path, variables: dict[str, str], data: dict[str, Any]) -> str:
178
- if not template_path.is_file():
179
- console.print(f"[red]Unable to locate the template at {template_path}")
180
- raise typer.Exit(1)
181
-
182
- templateLoader = jinja2.FileSystemLoader(searchpath=".")
183
- templateEnv = jinja2.Environment(loader=templateLoader, trim_blocks=True, lstrip_blocks=True)
184
- template = templateEnv.get_template(str(template_path))
185
-
179
+ async def render_jinja2_template(template_path: Path, variables: dict[str, Any], data: dict[str, Any]) -> str:
180
+ variables["data"] = data
181
+ jinja_template = Jinja2Template(template=Path(template_path), template_directory=Path())
186
182
  try:
187
- rendered_tpl = template.render(**variables, data=data) # type: ignore[arg-type]
188
- except jinja2.TemplateSyntaxError as exc:
189
- console.print("[red]Syntax Error detected on the template")
190
- console.print(f"[yellow] {exc}")
191
- raise typer.Exit(1) from exc
192
-
193
- except jinja2.UndefinedError as exc:
194
- console.print("[red]An error occurred while rendering the jinja template")
195
- traceback = Traceback(show_locals=False)
196
- errors = identify_faulty_jinja_code(traceback=traceback)
197
- for frame, syntax in errors:
198
- console.print(f"[yellow]{frame.filename} on line {frame.lineno}\n")
199
- console.print(syntax)
200
- console.print("")
201
- console.print(traceback.trace.stacks[0].exc_value)
183
+ rendered_tpl = await jinja_template.render(variables=variables)
184
+ except JinjaTemplateError as exc:
185
+ print_template_errors(error=exc, console=console)
202
186
  raise typer.Exit(1) from exc
203
187
 
204
188
  return rendered_tpl
205
189
 
206
190
 
207
- def _run_transform(
191
+ async def _run_transform(
208
192
  query_name: str,
209
193
  variables: dict[str, Any],
210
194
  transform_func: Callable,
@@ -227,7 +211,11 @@ def _run_transform(
227
211
 
228
212
  try:
229
213
  response = execute_graphql_query(
230
- query=query_name, variables_dict=variables, branch=branch, debug=debug, repository_config=repository_config
214
+ query=query_name,
215
+ variables_dict=variables,
216
+ branch=branch,
217
+ debug=debug,
218
+ repository_config=repository_config,
231
219
  )
232
220
 
233
221
  # TODO: response is a dict and can't be printed to the console in this way.
@@ -249,7 +237,7 @@ def _run_transform(
249
237
  raise typer.Abort()
250
238
 
251
239
  if asyncio.iscoroutinefunction(transform_func):
252
- output = asyncio.run(transform_func(response))
240
+ output = await transform_func(response)
253
241
  else:
254
242
  output = transform_func(response)
255
243
  return output
@@ -257,7 +245,7 @@ def _run_transform(
257
245
 
258
246
  @app.command(name="render")
259
247
  @catch_exception(console=console)
260
- def render(
248
+ async def render(
261
249
  transform_name: str = typer.Argument(default="", help="Name of the Python transformation", show_default=False),
262
250
  variables: Optional[list[str]] = typer.Argument(
263
251
  None, help="Variables to pass along with the query. Format key=value key=value."
@@ -289,7 +277,7 @@ def render(
289
277
  transform_func = functools.partial(render_jinja2_template, transform_config.template_path, variables_dict)
290
278
 
291
279
  # Query GQL and run the transform
292
- result = _run_transform(
280
+ result = await _run_transform(
293
281
  query_name=transform_config.query,
294
282
  variables=variables_dict,
295
283
  transform_func=transform_func,
@@ -410,7 +398,10 @@ def version() -> None:
410
398
 
411
399
  @app.command(name="info")
412
400
  @catch_exception(console=console)
413
- def info(detail: bool = typer.Option(False, help="Display detailed information."), _: str = CONFIG_PARAM) -> None: # noqa: PLR0915
401
+ def info( # noqa: PLR0915
402
+ detail: bool = typer.Option(False, help="Display detailed information."),
403
+ _: str = CONFIG_PARAM,
404
+ ) -> None:
414
405
  """Display the status of the Python SDK."""
415
406
 
416
407
  info: dict[str, Any] = {
@@ -476,10 +467,14 @@ def info(detail: bool = typer.Option(False, help="Display detailed information."
476
467
  infrahub_info = Table(show_header=False, box=None)
477
468
  if info["user_info"]:
478
469
  infrahub_info.add_row("User:", info["user_info"]["AccountProfile"]["display_label"])
479
- infrahub_info.add_row("Description:", info["user_info"]["AccountProfile"]["description"]["value"])
470
+ infrahub_info.add_row(
471
+ "Description:",
472
+ info["user_info"]["AccountProfile"]["description"]["value"],
473
+ )
480
474
  infrahub_info.add_row("Status:", info["user_info"]["AccountProfile"]["status"]["label"])
481
475
  infrahub_info.add_row(
482
- "Number of Groups:", str(info["user_info"]["AccountProfile"]["member_of_groups"]["count"])
476
+ "Number of Groups:",
477
+ str(info["user_info"]["AccountProfile"]["member_of_groups"]["count"]),
483
478
  )
484
479
 
485
480
  if groups := info["groups"]:
@@ -1,6 +1,12 @@
1
1
  from rich.console import Console
2
2
 
3
3
  from ..schema.repository import InfrahubRepositoryConfig
4
+ from ..template.exceptions import (
5
+ JinjaTemplateError,
6
+ JinjaTemplateNotFoundError,
7
+ JinjaTemplateSyntaxError,
8
+ JinjaTemplateUndefinedError,
9
+ )
4
10
 
5
11
 
6
12
  def list_jinja2_transforms(config: InfrahubRepositoryConfig) -> None:
@@ -9,3 +15,36 @@ def list_jinja2_transforms(config: InfrahubRepositoryConfig) -> None:
9
15
 
10
16
  for transform in config.jinja2_transforms:
11
17
  console.print(f"{transform.name} ({transform.template_path})")
18
+
19
+
20
+ def print_template_errors(error: JinjaTemplateError, console: Console) -> None:
21
+ if isinstance(error, JinjaTemplateNotFoundError):
22
+ console.print("[red]An error occurred while rendering the jinja template")
23
+ console.print("")
24
+ if error.base_template:
25
+ console.print(f"Base template: [yellow]{error.base_template}")
26
+ console.print(f"Missing template: [yellow]{error.filename}")
27
+ return
28
+
29
+ if isinstance(error, JinjaTemplateUndefinedError):
30
+ console.print("[red]An error occurred while rendering the jinja template")
31
+ for current_error in error.errors:
32
+ console.print(f"[yellow]{current_error.frame.filename} on line {current_error.frame.lineno}\n")
33
+ console.print(current_error.syntax)
34
+ console.print("")
35
+ console.print(error.message)
36
+ return
37
+
38
+ if isinstance(error, JinjaTemplateSyntaxError):
39
+ console.print("[red]A syntax error was encountered within the template")
40
+ console.print("")
41
+ if error.filename:
42
+ console.print(f"Filename: [yellow]{error.filename}")
43
+ console.print(f"Line number: [yellow]{error.lineno}")
44
+ console.print()
45
+ console.print(error.message)
46
+ return
47
+
48
+ console.print("[red]An error occurred while rendering the jinja template")
49
+ console.print("")
50
+ console.print(f"[yellow]{error.message}")
@@ -69,12 +69,12 @@ class ModuleImportError(Error):
69
69
  class NodeNotFoundError(Error):
70
70
  def __init__(
71
71
  self,
72
- node_type: str,
73
72
  identifier: Mapping[str, list[str]],
74
73
  message: str = "Unable to find the node in the database.",
75
74
  branch_name: str | None = None,
75
+ node_type: str | None = None,
76
76
  ):
77
- self.node_type = node_type
77
+ self.node_type = node_type or "unknown"
78
78
  self.identifier = identifier
79
79
  self.branch_name = branch_name
80
80
 
@@ -88,6 +88,10 @@ class NodeNotFoundError(Error):
88
88
  """
89
89
 
90
90
 
91
+ class NodeInvalidError(NodeNotFoundError):
92
+ pass
93
+
94
+
91
95
  class ResourceNotDefinedError(Error):
92
96
  """Raised when trying to access a resource that hasn't been defined."""
93
97
 
infrahub_sdk/generator.py CHANGED
@@ -137,7 +137,7 @@ class InfrahubGenerator:
137
137
 
138
138
  for node in self._nodes + self._related_nodes:
139
139
  if node.id:
140
- self._init_client.store.set(key=node.id, node=node)
140
+ self._init_client.store.set(node=node)
141
141
 
142
142
  @abstractmethod
143
143
  async def generate(self, data: dict) -> None:
infrahub_sdk/node.py CHANGED
@@ -15,7 +15,7 @@ from .exceptions import (
15
15
  )
16
16
  from .graphql import Mutation, Query
17
17
  from .schema import GenericSchemaAPI, RelationshipCardinality, RelationshipKind
18
- from .utils import compare_lists, get_flat_value
18
+ from .utils import compare_lists, generate_short_id, get_flat_value
19
19
  from .uuidt import UUIDT
20
20
 
21
21
  if TYPE_CHECKING:
@@ -43,6 +43,20 @@ ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE = (
43
43
  "calling generate is only supported for CoreArtifactDefinition nodes"
44
44
  )
45
45
 
46
+ HFID_STR_SEPARATOR = "__"
47
+
48
+
49
+ def parse_human_friendly_id(hfid: str | list[str]) -> tuple[str | None, list[str]]:
50
+ """Parse a human friendly ID into a kind and an identifier."""
51
+ if isinstance(hfid, str):
52
+ hfid_parts = hfid.split(HFID_STR_SEPARATOR)
53
+ if len(hfid_parts) == 1:
54
+ return None, hfid_parts
55
+ return hfid_parts[0], hfid_parts[1:]
56
+ if isinstance(hfid, list):
57
+ return None, hfid
58
+ raise ValueError(f"Invalid human friendly ID: {hfid}")
59
+
46
60
 
47
61
  class Attribute:
48
62
  """Represents an attribute of a Node, including its schema, value, and properties."""
@@ -340,10 +354,10 @@ class RelatedNode(RelatedNodeBase):
340
354
  return self._peer # type: ignore[return-value]
341
355
 
342
356
  if self.id and self.typename:
343
- return self._client.store.get(key=self.id, kind=self.typename) # type: ignore[return-value]
357
+ return self._client.store.get(key=self.id, kind=self.typename, branch=self._branch) # type: ignore[return-value]
344
358
 
345
359
  if self.hfid_str:
346
- return self._client.store.get_by_hfid(key=self.hfid_str) # type: ignore[return-value]
360
+ return self._client.store.get(key=self.hfid_str, branch=self._branch) # type: ignore[return-value]
347
361
 
348
362
  raise ValueError("Node must have at least one identifier (ID or HFID) to query it.")
349
363
 
@@ -387,10 +401,10 @@ class RelatedNodeSync(RelatedNodeBase):
387
401
  return self._peer # type: ignore[return-value]
388
402
 
389
403
  if self.id and self.typename:
390
- return self._client.store.get(key=self.id, kind=self.typename) # type: ignore[return-value]
404
+ return self._client.store.get(key=self.id, kind=self.typename, branch=self._branch) # type: ignore[return-value]
391
405
 
392
406
  if self.hfid_str:
393
- return self._client.store.get_by_hfid(key=self.hfid_str) # type: ignore[return-value]
407
+ return self._client.store.get(key=self.hfid_str, branch=self._branch) # type: ignore[return-value]
394
408
 
395
409
  raise ValueError("Node must have at least one identifier (ID or HFID) to query it.")
396
410
 
@@ -678,6 +692,11 @@ class InfrahubNodeBase:
678
692
  self._branch = branch
679
693
  self._existing: bool = True
680
694
 
695
+ # Generate a unique ID only to be used inside the SDK
696
+ # The format if this ID is purposely different from the ID used by the API
697
+ # This is done to avoid confusion and potential conflicts between the IDs
698
+ self._internal_id = generate_short_id()
699
+
681
700
  self.id = data.get("id", None) if isinstance(data, dict) else None
682
701
  self.display_label: str | None = data.get("display_label", None) if isinstance(data, dict) else None
683
702
  self.typename: str | None = data.get("__typename", schema.kind) if isinstance(data, dict) else schema.kind
@@ -694,6 +713,9 @@ class InfrahubNodeBase:
694
713
  self._init_attributes(data)
695
714
  self._init_relationships(data)
696
715
 
716
+ def get_branch(self) -> str:
717
+ return self._branch
718
+
697
719
  def get_path_value(self, path: str) -> Any:
698
720
  path_parts = path.split("__")
699
721
  return_value = None
@@ -794,6 +816,11 @@ class InfrahubNodeBase:
794
816
  def get_kind(self) -> str:
795
817
  return self._schema.kind
796
818
 
819
+ def get_all_kinds(self) -> list[str]:
820
+ if hasattr(self._schema, "inherit_from"):
821
+ return [self._schema.kind] + self._schema.inherit_from
822
+ return [self._schema.kind]
823
+
797
824
  def is_ip_prefix(self) -> bool:
798
825
  builtin_ipprefix_kind = "BuiltinIPPrefix"
799
826
  return self.get_kind() == builtin_ipprefix_kind or builtin_ipprefix_kind in self._schema.inherit_from # type: ignore[union-attr]
@@ -1201,7 +1228,7 @@ class InfrahubNode(InfrahubNodeBase):
1201
1228
  else:
1202
1229
  await self._client.group_context.add_related_nodes(ids=[self.id], update_group_context=update_group_context)
1203
1230
 
1204
- self._client.store.set(key=self.id, node=self)
1231
+ self._client.store.set(node=self)
1205
1232
 
1206
1233
  async def generate_query_data(
1207
1234
  self,
@@ -1479,15 +1506,15 @@ class InfrahubNode(InfrahubNodeBase):
1479
1506
  for rel_name in self._relationships:
1480
1507
  rel = getattr(self, rel_name)
1481
1508
  if rel and isinstance(rel, RelatedNode):
1482
- relation = node_data["node"].get(rel_name)
1483
- if relation.get("node", None):
1509
+ relation = node_data["node"].get(rel_name, None)
1510
+ if relation and relation.get("node", None):
1484
1511
  related_node = await InfrahubNode.from_graphql(
1485
1512
  client=self._client, branch=branch, data=relation, timeout=timeout
1486
1513
  )
1487
1514
  related_nodes.append(related_node)
1488
1515
  elif rel and isinstance(rel, RelationshipManager):
1489
- peers = node_data["node"].get(rel_name)
1490
- if peers:
1516
+ peers = node_data["node"].get(rel_name, None)
1517
+ if peers and peers["edges"]:
1491
1518
  for peer in peers["edges"]:
1492
1519
  related_node = await InfrahubNode.from_graphql(
1493
1520
  client=self._client, branch=branch, data=peer, timeout=timeout
@@ -1726,7 +1753,7 @@ class InfrahubNodeSync(InfrahubNodeBase):
1726
1753
  else:
1727
1754
  self._client.group_context.add_related_nodes(ids=[self.id], update_group_context=update_group_context)
1728
1755
 
1729
- self._client.store.set(key=self.id, node=self)
1756
+ self._client.store.set(node=self)
1730
1757
 
1731
1758
  def generate_query_data(
1732
1759
  self,
@@ -144,7 +144,8 @@ class AnyAttributeOptional(Attribute):
144
144
  @runtime_checkable
145
145
  class CoreNodeBase(Protocol):
146
146
  _schema: MainSchemaTypes
147
- id: str
147
+ _internal_id: str
148
+ id: str # NOTE this is incorrect, should be str | None
148
149
  display_label: str | None
149
150
 
150
151
  @property
@@ -153,10 +154,16 @@ class CoreNodeBase(Protocol):
153
154
  @property
154
155
  def hfid_str(self) -> str | None: ...
155
156
 
157
+ def get_human_friendly_id(self) -> list[str] | None: ...
158
+
156
159
  def get_human_friendly_id_as_string(self, include_kind: bool = False) -> str | None: ...
157
160
 
158
161
  def get_kind(self) -> str: ...
159
162
 
163
+ def get_all_kinds(self) -> list[str]: ...
164
+
165
+ def get_branch(self) -> str: ...
166
+
160
167
  def is_ip_prefix(self) -> bool: ...
161
168
 
162
169
  def is_ip_address(self) -> bool: ...
@@ -1,51 +1,47 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import difflib
5
+ from pathlib import Path
4
6
  from typing import TYPE_CHECKING, Any
5
7
 
6
8
  import jinja2
7
9
  import ujson
8
10
  from httpx import HTTPStatusError
9
- from rich.console import Console
10
- from rich.traceback import Traceback
11
11
 
12
- from ...jinja2 import identify_faulty_jinja_code
13
- from ..exceptions import Jinja2TransformError, Jinja2TransformUndefinedError, OutputMatchError
12
+ from ...template import Jinja2Template
13
+ from ...template.exceptions import JinjaTemplateError
14
+ from ..exceptions import OutputMatchError
14
15
  from ..models import InfrahubInputOutputTest, InfrahubTestExpectedResult
15
16
  from .base import InfrahubItem
16
17
 
17
18
  if TYPE_CHECKING:
18
- from pathlib import Path
19
-
20
19
  from pytest import ExceptionInfo
21
20
 
22
21
 
23
22
  class InfrahubJinja2Item(InfrahubItem):
23
+ def _get_jinja2(self) -> Jinja2Template:
24
+ return Jinja2Template(
25
+ template=Path(self.resource_config.template_path), # type: ignore[attr-defined]
26
+ template_directory=Path(self.session.infrahub_config_path.parent), # type: ignore[attr-defined]
27
+ )
28
+
24
29
  def get_jinja2_environment(self) -> jinja2.Environment:
25
- loader = jinja2.FileSystemLoader(self.session.infrahub_config_path.parent) # type: ignore[attr-defined]
26
- return jinja2.Environment(loader=loader, trim_blocks=True, lstrip_blocks=True)
30
+ jinja2_template = self._get_jinja2()
31
+ return jinja2_template.get_environment()
27
32
 
28
33
  def get_jinja2_template(self) -> jinja2.Template:
29
- return self.get_jinja2_environment().get_template(str(self.resource_config.template_path)) # type: ignore[attr-defined]
34
+ jinja2_template = self._get_jinja2()
35
+ return jinja2_template.get_template()
30
36
 
31
37
  def render_jinja2_template(self, variables: dict[str, Any]) -> str | None:
38
+ jinja2_template = self._get_jinja2()
39
+
32
40
  try:
33
- return self.get_jinja2_template().render(**variables)
34
- except jinja2.UndefinedError as exc:
35
- traceback = Traceback(show_locals=False)
36
- errors = identify_faulty_jinja_code(traceback=traceback)
37
- console = Console()
38
- with console.capture() as capture:
39
- console.print(f"An error occurred while rendering Jinja2 transform:{self.name!r}\n", soft_wrap=True)
40
- console.print(f"{exc.message}\n", soft_wrap=True)
41
- for frame, syntax in errors:
42
- console.print(f"{frame.filename} on line {frame.lineno}\n", soft_wrap=True)
43
- console.print(syntax, soft_wrap=True)
44
- str_output = capture.get()
41
+ return asyncio.run(jinja2_template.render(variables=variables))
42
+ except JinjaTemplateError as exc:
45
43
  if self.test.expect == InfrahubTestExpectedResult.PASS:
46
- raise Jinja2TransformUndefinedError(
47
- name=self.name, message=str_output, rtb=traceback, errors=errors
48
- ) from exc
44
+ raise exc
49
45
  return None
50
46
 
51
47
  def get_result_differences(self, computed: Any) -> str | None:
@@ -99,8 +95,8 @@ class InfrahubJinja2TransformUnitRenderItem(InfrahubJinja2Item):
99
95
  raise OutputMatchError(name=self.name, differences=differences)
100
96
 
101
97
  def repr_failure(self, excinfo: ExceptionInfo, style: str | None = None) -> str:
102
- if isinstance(excinfo.value, (Jinja2TransformUndefinedError, Jinja2TransformError)):
103
- return excinfo.value.message
98
+ if isinstance(excinfo.value, (JinjaTemplateError)):
99
+ return str(excinfo.value.message)
104
100
 
105
101
  return super().repr_failure(excinfo, style=style)
106
102
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import json
4
5
  from collections.abc import MutableMapping
5
6
  from enum import Enum
6
7
  from time import sleep
@@ -13,6 +14,7 @@ from typing_extensions import TypeAlias
13
14
 
14
15
  from ..exceptions import (
15
16
  InvalidResponseError,
17
+ JsonDecodeError,
16
18
  SchemaNotFoundError,
17
19
  ValidationError,
18
20
  )
@@ -420,7 +422,14 @@ class InfrahubSchema(InfrahubSchemaBase):
420
422
  response = await self.client._get(url=url, timeout=timeout)
421
423
  response.raise_for_status()
422
424
 
423
- data: MutableMapping[str, Any] = response.json()
425
+ try:
426
+ data: MutableMapping[str, Any] = response.json()
427
+ except json.decoder.JSONDecodeError as exc:
428
+ raise JsonDecodeError(
429
+ message=f"Invalid Schema response received from the server at {response.url}: {response.text} [{response.status_code}] ",
430
+ content=response.text,
431
+ url=response.url,
432
+ ) from exc
424
433
 
425
434
  nodes: MutableMapping[str, MainSchemaTypesAPI] = {}
426
435
  for node_schema in data.get("nodes", []):