infrahub-server 1.7.0b0__py3-none-any.whl → 1.7.1__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 (177) hide show
  1. infrahub/api/exceptions.py +2 -2
  2. infrahub/api/schema.py +5 -0
  3. infrahub/cli/db.py +54 -24
  4. infrahub/core/account.py +12 -9
  5. infrahub/core/branch/models.py +11 -117
  6. infrahub/core/branch/tasks.py +7 -3
  7. infrahub/core/diff/branch_differ.py +1 -1
  8. infrahub/core/diff/conflict_transferer.py +1 -1
  9. infrahub/core/diff/data_check_synchronizer.py +1 -1
  10. infrahub/core/diff/enricher/cardinality_one.py +1 -1
  11. infrahub/core/diff/enricher/hierarchy.py +1 -1
  12. infrahub/core/diff/enricher/labels.py +1 -1
  13. infrahub/core/diff/merger/merger.py +6 -2
  14. infrahub/core/diff/repository/repository.py +3 -1
  15. infrahub/core/graph/__init__.py +1 -1
  16. infrahub/core/graph/constraints.py +1 -1
  17. infrahub/core/initialization.py +2 -1
  18. infrahub/core/ipam/reconciler.py +8 -6
  19. infrahub/core/ipam/utilization.py +8 -15
  20. infrahub/core/manager.py +1 -26
  21. infrahub/core/merge.py +1 -1
  22. infrahub/core/migrations/graph/__init__.py +2 -0
  23. infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
  24. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +4 -4
  25. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +3 -2
  26. infrahub/core/migrations/graph/m015_diff_format_update.py +3 -2
  27. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +3 -2
  28. infrahub/core/migrations/graph/m017_add_core_profile.py +6 -4
  29. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +3 -4
  30. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -3
  31. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +3 -4
  32. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +4 -5
  33. infrahub/core/migrations/graph/m028_delete_diffs.py +3 -2
  34. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +3 -2
  35. infrahub/core/migrations/graph/m031_check_number_attributes.py +4 -3
  36. infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +3 -2
  37. infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +3 -2
  38. infrahub/core/migrations/graph/m035_orphan_relationships.py +3 -3
  39. infrahub/core/migrations/graph/m036_drop_attr_value_index.py +3 -2
  40. infrahub/core/migrations/graph/m037_index_attr_vals.py +3 -2
  41. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +4 -5
  42. infrahub/core/migrations/graph/m039_ipam_reconcile.py +3 -2
  43. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +4 -3
  44. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +5 -4
  45. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +12 -5
  46. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +15 -4
  47. infrahub/core/migrations/graph/m045_backfill_hfid_display_label_in_db_profile_template.py +10 -4
  48. infrahub/core/migrations/graph/m046_fill_agnostic_hfid_display_labels.py +6 -5
  49. infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +19 -5
  50. infrahub/core/migrations/graph/m048_undelete_rel_props.py +6 -4
  51. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +19 -4
  52. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +3 -3
  53. infrahub/core/migrations/graph/m051_subtract_branched_from_microsecond.py +39 -0
  54. infrahub/core/migrations/query/__init__.py +2 -2
  55. infrahub/core/migrations/query/schema_attribute_update.py +1 -1
  56. infrahub/core/migrations/runner.py +6 -3
  57. infrahub/core/migrations/schema/attribute_kind_update.py +8 -11
  58. infrahub/core/migrations/schema/attribute_name_update.py +1 -1
  59. infrahub/core/migrations/schema/attribute_supports_profile.py +5 -10
  60. infrahub/core/migrations/schema/models.py +8 -0
  61. infrahub/core/migrations/schema/node_attribute_add.py +11 -14
  62. infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
  63. infrahub/core/migrations/schema/node_kind_update.py +1 -1
  64. infrahub/core/migrations/schema/tasks.py +7 -1
  65. infrahub/core/migrations/shared.py +37 -30
  66. infrahub/core/node/__init__.py +3 -2
  67. infrahub/core/node/base.py +9 -5
  68. infrahub/core/node/delete_validator.py +1 -1
  69. infrahub/core/order.py +30 -0
  70. infrahub/core/protocols.py +1 -0
  71. infrahub/core/protocols_base.py +4 -0
  72. infrahub/core/query/__init__.py +8 -5
  73. infrahub/core/query/attribute.py +3 -3
  74. infrahub/core/query/branch.py +1 -1
  75. infrahub/core/query/delete.py +1 -1
  76. infrahub/core/query/diff.py +3 -3
  77. infrahub/core/query/ipam.py +104 -43
  78. infrahub/core/query/node.py +454 -101
  79. infrahub/core/query/relationship.py +83 -26
  80. infrahub/core/query/resource_manager.py +107 -18
  81. infrahub/core/relationship/constraints/count.py +1 -1
  82. infrahub/core/relationship/constraints/peer_kind.py +1 -1
  83. infrahub/core/relationship/constraints/peer_parent.py +1 -1
  84. infrahub/core/relationship/constraints/peer_relatives.py +1 -1
  85. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  86. infrahub/core/relationship/constraints/profiles_removal.py +1 -1
  87. infrahub/core/relationship/model.py +8 -2
  88. infrahub/core/schema/attribute_parameters.py +28 -1
  89. infrahub/core/schema/attribute_schema.py +9 -15
  90. infrahub/core/schema/basenode_schema.py +3 -0
  91. infrahub/core/schema/definitions/core/__init__.py +8 -2
  92. infrahub/core/schema/definitions/core/account.py +10 -10
  93. infrahub/core/schema/definitions/core/artifact.py +14 -8
  94. infrahub/core/schema/definitions/core/check.py +10 -4
  95. infrahub/core/schema/definitions/core/generator.py +26 -6
  96. infrahub/core/schema/definitions/core/graphql_query.py +1 -1
  97. infrahub/core/schema/definitions/core/group.py +9 -2
  98. infrahub/core/schema/definitions/core/ipam.py +80 -10
  99. infrahub/core/schema/definitions/core/menu.py +41 -7
  100. infrahub/core/schema/definitions/core/permission.py +16 -2
  101. infrahub/core/schema/definitions/core/profile.py +16 -2
  102. infrahub/core/schema/definitions/core/propose_change.py +24 -4
  103. infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
  104. infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
  105. infrahub/core/schema/definitions/core/repository.py +10 -0
  106. infrahub/core/schema/definitions/core/resource_pool.py +8 -1
  107. infrahub/core/schema/definitions/core/template.py +19 -2
  108. infrahub/core/schema/definitions/core/transform.py +11 -5
  109. infrahub/core/schema/definitions/core/webhook.py +27 -9
  110. infrahub/core/schema/manager.py +50 -38
  111. infrahub/core/schema/schema_branch.py +68 -2
  112. infrahub/core/utils.py +3 -3
  113. infrahub/core/validators/aggregated_checker.py +1 -1
  114. infrahub/core/validators/attribute/choices.py +1 -1
  115. infrahub/core/validators/attribute/enum.py +1 -1
  116. infrahub/core/validators/attribute/kind.py +6 -3
  117. infrahub/core/validators/attribute/length.py +1 -1
  118. infrahub/core/validators/attribute/min_max.py +1 -1
  119. infrahub/core/validators/attribute/number_pool.py +1 -1
  120. infrahub/core/validators/attribute/optional.py +1 -1
  121. infrahub/core/validators/attribute/regex.py +1 -1
  122. infrahub/core/validators/node/attribute.py +1 -1
  123. infrahub/core/validators/node/relationship.py +1 -1
  124. infrahub/core/validators/relationship/peer.py +1 -1
  125. infrahub/database/__init__.py +1 -1
  126. infrahub/git/utils.py +1 -1
  127. infrahub/graphql/app.py +2 -2
  128. infrahub/graphql/field_extractor.py +1 -1
  129. infrahub/graphql/manager.py +17 -3
  130. infrahub/graphql/mutations/account.py +1 -1
  131. infrahub/graphql/order.py +14 -0
  132. infrahub/graphql/queries/diff/tree.py +5 -5
  133. infrahub/graphql/queries/resource_manager.py +25 -24
  134. infrahub/graphql/resolvers/ipam.py +3 -3
  135. infrahub/graphql/resolvers/resolver.py +44 -3
  136. infrahub/graphql/types/standard_node.py +8 -4
  137. infrahub/lock.py +7 -0
  138. infrahub/menu/repository.py +1 -1
  139. infrahub/patch/queries/base.py +1 -1
  140. infrahub/pools/number.py +1 -8
  141. infrahub/profiles/node_applier.py +1 -1
  142. infrahub/profiles/queries/get_profile_data.py +1 -1
  143. infrahub/proposed_change/action_checker.py +1 -1
  144. infrahub/services/__init__.py +1 -1
  145. infrahub/services/adapters/cache/nats.py +1 -1
  146. infrahub/services/adapters/cache/redis.py +7 -0
  147. infrahub/webhook/gather.py +1 -1
  148. infrahub/webhook/tasks.py +22 -6
  149. infrahub_sdk/analyzer.py +2 -2
  150. infrahub_sdk/branch.py +12 -39
  151. infrahub_sdk/checks.py +4 -4
  152. infrahub_sdk/client.py +36 -0
  153. infrahub_sdk/ctl/cli_commands.py +2 -1
  154. infrahub_sdk/ctl/graphql.py +15 -4
  155. infrahub_sdk/ctl/utils.py +2 -2
  156. infrahub_sdk/enums.py +6 -0
  157. infrahub_sdk/graphql/renderers.py +21 -0
  158. infrahub_sdk/graphql/utils.py +85 -0
  159. infrahub_sdk/node/attribute.py +12 -2
  160. infrahub_sdk/node/constants.py +11 -0
  161. infrahub_sdk/node/metadata.py +69 -0
  162. infrahub_sdk/node/node.py +65 -14
  163. infrahub_sdk/node/property.py +3 -0
  164. infrahub_sdk/node/related_node.py +24 -1
  165. infrahub_sdk/node/relationship.py +10 -1
  166. infrahub_sdk/operation.py +2 -2
  167. infrahub_sdk/schema/repository.py +1 -2
  168. infrahub_sdk/transforms.py +2 -2
  169. infrahub_sdk/types.py +18 -2
  170. {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/METADATA +6 -6
  171. {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/RECORD +176 -172
  172. {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/entry_points.txt +0 -1
  173. infrahub_testcontainers/models.py +3 -3
  174. infrahub_testcontainers/performance_test.py +1 -1
  175. infrahub/graphql/models.py +0 -36
  176. {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/WHEEL +0 -0
  177. {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
@@ -4,12 +4,12 @@ from infrahub.exceptions import Error
4
4
  class QueryValidationError(Error):
5
5
  HTTP_CODE = 400
6
6
 
7
- def __init__(self, message: str):
7
+ def __init__(self, message: str) -> None:
8
8
  self.message = message
9
9
 
10
10
 
11
11
  class SchemaNotValidError(Error):
12
12
  HTTP_CODE = 422
13
13
 
14
- def __init__(self, message: str):
14
+ def __init__(self, message: str) -> None:
15
15
  self.message = message
infrahub/api/schema.py CHANGED
@@ -36,6 +36,7 @@ from infrahub.core.schema import (
36
36
  TemplateSchema,
37
37
  )
38
38
  from infrahub.core.schema.constants import SchemaNamespace # noqa: TC001
39
+ from infrahub.core.timestamp import Timestamp
39
40
  from infrahub.core.validators.models.validate_migration import (
40
41
  SchemaValidateMigrationData,
41
42
  SchemaValidatorPathResponseData,
@@ -353,6 +354,8 @@ async def load_schema(
353
354
  if error_messages:
354
355
  raise SchemaNotValidError(",\n".join(error_messages))
355
356
 
357
+ schema_load_at = Timestamp()
358
+
356
359
  # ----------------------------------------------------------
357
360
  # Update the schema
358
361
  # ----------------------------------------------------------
@@ -367,6 +370,7 @@ async def load_schema(
367
370
  limit=result.diff.all,
368
371
  update_db=True,
369
372
  user_id=account_session.account_id,
373
+ at=schema_load_at,
370
374
  )
371
375
  branch.update_schema_hash()
372
376
  log.info("Schema has been updated", branch=branch.name, hash=branch.active_schema_hash.main)
@@ -389,6 +393,7 @@ async def load_schema(
389
393
  previous_schema=origin_schema,
390
394
  migrations=result.migrations,
391
395
  user_id=account_session.account_id,
396
+ at=schema_load_at,
392
397
  )
393
398
  migration_error_msgs = await service.workflow.execute_workflow(
394
399
  workflow=SCHEMA_APPLY_MIGRATION,
infrahub/cli/db.py CHANGED
@@ -40,10 +40,11 @@ from infrahub.core.migrations.exceptions import MigrationFailureError
40
40
  from infrahub.core.migrations.graph import get_graph_migrations, get_migration_by_number
41
41
  from infrahub.core.migrations.schema.models import SchemaApplyMigrationData
42
42
  from infrahub.core.migrations.schema.tasks import schema_apply_migrations
43
- from infrahub.core.migrations.shared import get_migration_console
43
+ from infrahub.core.migrations.shared import MigrationInput, get_migration_console
44
44
  from infrahub.core.schema import SchemaRoot, core_models, internal_schema
45
45
  from infrahub.core.schema.definitions.deprecated import deprecated_models
46
46
  from infrahub.core.schema.manager import SchemaManager
47
+ from infrahub.core.timestamp import Timestamp
47
48
  from infrahub.core.validators.models.validate_migration import SchemaValidateMigrationData
48
49
  from infrahub.core.validators.tasks import schema_validate_migrations
49
50
  from infrahub.database import DatabaseType
@@ -65,6 +66,7 @@ def get_timestamp_string() -> str:
65
66
  if TYPE_CHECKING:
66
67
  from infrahub.cli.context import CliContext
67
68
  from infrahub.core.migrations.shared import MigrationTypes
69
+ from infrahub.core.root import Root
68
70
  from infrahub.database import InfrahubDatabase
69
71
  from infrahub.database.index import IndexManagerBase
70
72
 
@@ -93,12 +95,40 @@ def callback() -> None:
93
95
  """
94
96
 
95
97
 
98
+ async def do_migrate(
99
+ db: InfrahubDatabase,
100
+ root_node: Root,
101
+ check: bool = False,
102
+ migration_number: int | None = None,
103
+ ) -> None:
104
+ """Core migration logic that can be called independently of CLI.
105
+
106
+ Args:
107
+ db: The database connection.
108
+ root_node: The root node containing the current graph version.
109
+ check: If True, only check which migrations need to run without applying them.
110
+ migration_number: If provided, run only this specific migration.
111
+ """
112
+ migrations = await detect_migration_to_run(
113
+ current_graph_version=root_node.graph_version, migration_number=migration_number
114
+ )
115
+
116
+ if check or not migrations:
117
+ return
118
+
119
+ await migrate_database(
120
+ db=db, migrations=migrations, initialize=True, update_graph_version=(migration_number is None)
121
+ )
122
+
123
+
96
124
  @app.command(name="migrate")
97
125
  async def migrate_cmd(
98
126
  ctx: typer.Context,
99
127
  check: bool = typer.Option(False, help="Check the state of the database without applying the migrations."),
100
128
  config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
101
- migration_number: int | None = typer.Option(None, help="Apply a specific migration by number"),
129
+ migration_number: int | None = typer.Option(
130
+ None, help="Apply a specific migration by number, regardless of current database version"
131
+ ),
102
132
  ) -> None:
103
133
  """Check the current format of the internal graph and apply the necessary migrations"""
104
134
  logging.getLogger("infrahub").setLevel(logging.WARNING)
@@ -111,14 +141,7 @@ async def migrate_cmd(
111
141
  dbdriver = await context.init_db(retry=1)
112
142
 
113
143
  root_node = await get_root_node(db=dbdriver)
114
- migrations = await detect_migration_to_run(
115
- current_graph_version=root_node.graph_version, migration_number=migration_number
116
- )
117
-
118
- if check or not migrations:
119
- return
120
-
121
- await migrate_database(db=dbdriver, migrations=migrations, initialize=True)
144
+ await do_migrate(db=dbdriver, root_node=root_node, check=check, migration_number=migration_number)
122
145
 
123
146
  await dbdriver.close()
124
147
 
@@ -292,18 +315,17 @@ async def detect_migration_to_run(
292
315
  migration = get_migration_by_number(migration_number)
293
316
  migrations.append(migration)
294
317
  if current_graph_version > migration.minimum_version:
318
+ get_migration_console().log(f"Migration {migration_number} will be re-applied.")
319
+ else:
295
320
  get_migration_console().log(
296
- f"Migration {migration_number} already applied. To apply again, run the command without the --check flag."
321
+ f"Migration {migration_number} needs to be applied. Run `infrahub db migrate` to apply all outstanding migrations."
297
322
  )
298
- return []
299
- get_migration_console().log(
300
- f"Migration {migration_number} needs to be applied. Run `infrahub db migrate` to apply all outstanding migrations."
301
- )
302
- else:
303
- migrations.extend(await get_graph_migrations(current_graph_version=current_graph_version))
304
- if not migrations:
305
- get_migration_console().log(f"Database up-to-date (v{current_graph_version}), no migration to execute.")
306
- return []
323
+ return migrations
324
+
325
+ migrations.extend(await get_graph_migrations(current_graph_version=current_graph_version))
326
+ if not migrations:
327
+ get_migration_console().log(f"Database up-to-date (v{current_graph_version}), no migration to execute.")
328
+ return []
307
329
 
308
330
  get_migration_console().log(
309
331
  f"Database needs to be updated (v{current_graph_version} -> v{GRAPH_VERSION}), {len(migrations)} migrations pending"
@@ -312,7 +334,10 @@ async def detect_migration_to_run(
312
334
 
313
335
 
314
336
  async def migrate_database(
315
- db: InfrahubDatabase, migrations: Sequence[MigrationTypes], initialize: bool = False
337
+ db: InfrahubDatabase,
338
+ migrations: Sequence[MigrationTypes],
339
+ initialize: bool = False,
340
+ update_graph_version: bool = True,
316
341
  ) -> bool:
317
342
  """Apply the latest migrations to the database, this function will print the status directly in the console.
318
343
 
@@ -322,6 +347,7 @@ async def migrate_database(
322
347
  db: The database object.
323
348
  migrations: Sequence of migrations to apply.
324
349
  initialize: Whether to initialize the registry before running migrations.
350
+ update_graph_version: Whether to update the graph version after each migration.
325
351
  """
326
352
  if not migrations:
327
353
  return True
@@ -332,15 +358,16 @@ async def migrate_database(
332
358
  root_node = await get_root_node(db=db)
333
359
 
334
360
  for migration in migrations:
335
- execution_result = await migration.execute(db=db)
361
+ execution_result = await migration.execute(migration_input=MigrationInput(db=db))
336
362
  validation_result = None
337
363
 
338
364
  if execution_result.success:
339
365
  validation_result = await migration.validate_migration(db=db)
340
366
  if validation_result.success:
341
367
  get_migration_console().log(f"Migration: {migration.name} {SUCCESS_BADGE}")
342
- root_node.graph_version = migration.minimum_version + 1
343
- await root_node.save(db=db)
368
+ if update_graph_version:
369
+ root_node.graph_version = migration.minimum_version + 1
370
+ await root_node.save(db=db)
344
371
 
345
372
  if not execution_result.success or (validation_result and not validation_result.success):
346
373
  get_migration_console().log(f"Migration: {migration.name} {FAILED_BADGE}")
@@ -476,6 +503,7 @@ async def update_core_schema(db: InfrahubDatabase, initialize: bool = True, debu
476
503
  schema_default_branch.process()
477
504
  registry.schema.set_schema_branch(name=default_branch.name, schema=schema_default_branch)
478
505
 
506
+ update_at = Timestamp()
479
507
  async with db.start_transaction() as dbt:
480
508
  await registry.schema.update_schema_branch(
481
509
  schema=candidate_schema,
@@ -484,6 +512,7 @@ async def update_core_schema(db: InfrahubDatabase, initialize: bool = True, debu
484
512
  diff=result.diff,
485
513
  limit=result.diff.all,
486
514
  update_db=True,
515
+ at=update_at,
487
516
  )
488
517
  default_branch.update_schema_hash()
489
518
  get_migration_console().log(
@@ -501,6 +530,7 @@ async def update_core_schema(db: InfrahubDatabase, initialize: bool = True, debu
501
530
  new_schema=candidate_schema,
502
531
  previous_schema=origin_schema,
503
532
  migrations=result.migrations,
533
+ at=update_at,
504
534
  )
505
535
  migration_error_msgs = await schema_apply_migrations(message=apply_migration_data)
506
536
 
infrahub/core/account.py CHANGED
@@ -54,11 +54,15 @@ class AccountGlobalPermissionQuery(Query):
54
54
  name: str = "account_global_permissions"
55
55
  type: QueryType = QueryType.READ
56
56
 
57
- def __init__(self, account_id: str, **kwargs: Any):
57
+ def __init__(
58
+ self,
59
+ account_id: str,
60
+ branch: Branch | None = None,
61
+ branch_agnostic: bool = False,
62
+ ) -> None:
63
+ super().__init__(branch=branch, branch_agnostic=branch_agnostic)
58
64
  self.account_id = account_id
59
- super().__init__(**kwargs)
60
65
 
61
- async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
62
66
  self.params["account_id"] = self.account_id
63
67
 
64
68
  branch_filter, branch_params = self.branch.get_query_filter_path(
@@ -66,7 +70,6 @@ class AccountGlobalPermissionQuery(Query):
66
70
  )
67
71
  self.params.update(branch_params)
68
72
 
69
- # ruff: noqa: E501
70
73
  query = """
71
74
  MATCH (account:%(generic_account_node)s)
72
75
  WHERE account.uuid = $account_id
@@ -164,7 +167,7 @@ class AccountObjectPermissionQuery(Query):
164
167
  name: str = "account_object_permissions"
165
168
  type: QueryType = QueryType.READ
166
169
 
167
- def __init__(self, account_id: str, **kwargs: Any):
170
+ def __init__(self, account_id: str, **kwargs: Any) -> None:
168
171
  self.account_id = account_id
169
172
  super().__init__(**kwargs)
170
173
 
@@ -293,7 +296,7 @@ class AccountObjectPermissionQuery(Query):
293
296
 
294
297
 
295
298
  async def fetch_permissions(account_id: str, db: InfrahubDatabase, branch: Branch) -> AssignedPermissions:
296
- query1 = await AccountGlobalPermissionQuery.init(db=db, branch=branch, account_id=account_id, branch_agnostic=True)
299
+ query1 = AccountGlobalPermissionQuery(branch=branch, account_id=account_id, branch_agnostic=True)
297
300
  await query1.execute(db=db)
298
301
  global_permissions = query1.get_permissions()
299
302
 
@@ -308,7 +311,7 @@ class AccountRoleGlobalPermissionQuery(Query):
308
311
  name: str = "account_role_global_permissions"
309
312
  type: QueryType = QueryType.READ
310
313
 
311
- def __init__(self, role_id: str, **kwargs: Any):
314
+ def __init__(self, role_id: str, **kwargs: Any) -> None:
312
315
  self.role_id = role_id
313
316
  super().__init__(**kwargs)
314
317
 
@@ -394,7 +397,7 @@ class AccountRoleObjectPermissionQuery(Query):
394
397
  name: str = "account_role_object_permissions"
395
398
  type: QueryType = QueryType.READ
396
399
 
397
- def __init__(self, role_id: str, **kwargs: Any):
400
+ def __init__(self, role_id: str, **kwargs: Any) -> None:
398
401
  self.role_id = role_id
399
402
  super().__init__(**kwargs)
400
403
 
@@ -515,7 +518,7 @@ class AccountTokenValidateQuery(Query):
515
518
  name: str = "account_token_validate"
516
519
  type: QueryType = QueryType.READ
517
520
 
518
- def __init__(self, token: str, **kwargs: Any):
521
+ def __init__(self, token: str, **kwargs: Any) -> None:
519
522
  self.token = token
520
523
  super().__init__(**kwargs)
521
524
 
@@ -333,7 +333,7 @@ class Branch(StandardNode):
333
333
  f"({rel}.branch IN $branch{idx} AND {rel}.from <= $time{idx} AND {rel}.to IS NULL)"
334
334
  )
335
335
  filters_per_rel.append(
336
- f"({rel}.branch IN $branch{idx} AND {rel}.from <= $time{idx} AND {rel}.to >= $time{idx})"
336
+ f"({rel}.branch IN $branch{idx} AND {rel}.from <= $time{idx} AND {rel}.to > $time{idx})"
337
337
  )
338
338
 
339
339
  if not include_outside_parentheses:
@@ -366,7 +366,9 @@ class Branch(StandardNode):
366
366
  at = Timestamp(at)
367
367
  at_str = at.to_string()
368
368
  if branch_agnostic:
369
- filter_str = f"{variable_name}.from < ${pp}time1 AND ({variable_name}.to IS NULL or {variable_name}.to >= ${pp}time1)"
369
+ filter_str = (
370
+ f"{variable_name}.from < ${pp}time1 AND ({variable_name}.to IS NULL or {variable_name}.to > ${pp}time1)"
371
+ )
370
372
  params[f"{pp}time1"] = at_str
371
373
  return filter_str, params
372
374
 
@@ -380,132 +382,24 @@ class Branch(StandardNode):
380
382
  for idx in range(len(branches_times)):
381
383
  filters.append(
382
384
  f"({variable_name}.branch IN ${pp}branch{idx} "
383
- f"AND {variable_name}.from < ${pp}time{idx} AND {variable_name}.to IS NULL)"
385
+ f"AND {variable_name}.from <= ${pp}time{idx} AND {variable_name}.to IS NULL)"
384
386
  )
385
387
  filters.append(
386
388
  f"({variable_name}.branch IN ${pp}branch{idx} "
387
- f"AND {variable_name}.from < ${pp}time{idx} "
388
- f"AND {variable_name}.to >= ${pp}time{idx})"
389
+ f"AND {variable_name}.from <= ${pp}time{idx} "
390
+ f"AND {variable_name}.to > ${pp}time{idx})"
389
391
  )
390
392
 
391
393
  filter_str = "(" + "\n OR ".join(filters) + ")"
392
394
 
393
395
  return filter_str, params
394
396
 
395
- def get_query_filter_relationships_range(
396
- self,
397
- rel_labels: list,
398
- start_time: Timestamp,
399
- end_time: Timestamp,
400
- include_outside_parentheses: bool = False,
401
- include_global: bool = False,
402
- ) -> tuple[list, dict]:
403
- """Generate a CYPHER Query filter based on a list of relationships to query a range of values in the graph.
404
- The goal is to return all the values that are valid during this timerange.
405
- """
406
-
407
- filters = []
408
- params = {}
409
-
410
- if not isinstance(rel_labels, list):
411
- raise TypeError(f"rel_labels must be a list, not a {type(rel_labels)}")
412
-
413
- start_time = Timestamp(start_time)
414
- end_time = Timestamp(end_time)
415
-
416
- if include_global:
417
- branches_times = self.get_branches_and_times_to_query_global(at=start_time)
418
- else:
419
- branches_times = self.get_branches_and_times_to_query(at=start_time)
420
-
421
- params["branches"] = list({branch for branches in branches_times for branch in branches})
422
- params["start_time"] = start_time.to_string()
423
- params["end_time"] = end_time.to_string()
424
-
425
- for rel in rel_labels:
426
- filters_per_rel = [
427
- f"({rel}.branch IN $branches AND {rel}.from <= $end_time AND {rel}.to IS NULL)",
428
- f"({rel}.branch IN $branches AND ({rel}.from <= $end_time OR ({rel}.to >= $start_time AND {rel}.to <= $end_time)))",
429
- ]
430
-
431
- if not include_outside_parentheses:
432
- filters.append("\n OR ".join(filters_per_rel))
433
-
434
- filters.append("(" + "\n OR ".join(filters_per_rel) + ")")
435
-
436
- return filters, params
437
-
438
- def get_query_filter_relationships_diff(
439
- self, rel_labels: list, diff_from: Timestamp, diff_to: Timestamp
440
- ) -> tuple[list, dict]:
441
- """
442
- Generate a CYPHER Query filter to query all events that are applicable to a given branch based
443
- - The time when the branch as created
444
- - The branched_from time of the branch
445
- - The diff_to and diff_from time as provided
446
- """
447
-
448
- if not isinstance(rel_labels, list):
449
- raise TypeError(f"rel_labels must be a list, not a {type(rel_labels)}")
450
-
451
- start_times, end_times = self.get_branches_and_times_for_range(start_time=diff_from, end_time=diff_to)
452
-
453
- filters = []
454
- params = {}
455
-
456
- for idx, branch_name in enumerate(start_times.keys()):
457
- params[f"branch{idx}"] = branch_name
458
- params[f"start_time{idx}"] = start_times[branch_name]
459
- params[f"end_time{idx}"] = end_times[branch_name]
460
-
461
- for rel in rel_labels:
462
- filters_per_rel = []
463
- for idx in range(len(start_times)):
464
- filters_per_rel.extend(
465
- [
466
- f"""({rel}.branch = $branch{idx}
467
- AND {rel}.from >= $start_time{idx}
468
- AND {rel}.from <= $end_time{idx}
469
- AND ( r2.to is NULL or r2.to >= $end_time{idx}))""",
470
- f"""({rel}.branch = $branch{idx} AND {rel}.from >= $start_time{idx}
471
- AND {rel}.to <= $start_time{idx})""",
472
- ]
473
- )
474
-
475
- filters.append("(" + "\n OR ".join(filters_per_rel) + ")")
476
-
477
- return filters, params
478
-
479
- def get_query_filter_range(self, rel_label: list, start_time: Timestamp, end_time: Timestamp) -> tuple[list, dict]:
480
- """
481
- Generate a CYPHER Query filter to query a range of values in the graph between start_time and end_time."""
482
-
483
- filters = []
484
- params = {}
485
-
486
- start_time = Timestamp(start_time)
487
- end_time = Timestamp(end_time)
488
-
489
- params["branches"] = self.get_branches_in_scope()
490
- params["start_time"] = start_time.to_string()
491
- params["end_time"] = end_time.to_string()
492
-
493
- filters_per_rel = [
494
- f"""({rel_label}.branch IN $branches AND {rel_label}.from >= $start_time
495
- AND {rel_label}.from <= $end_time AND {rel_label}.to IS NULL)""",
496
- f"""({rel_label}.branch IN $branches AND (({rel_label}.from >= $start_time
497
- AND {rel_label}.from <= $end_time) OR ({rel_label}.to >= $start_time
498
- AND {rel_label}.to <= $end_time)))""",
499
- ]
500
-
501
- filters.append("(" + "\n OR ".join(filters_per_rel) + ")")
502
-
503
- return filters, params
504
-
505
- async def rebase(self, db: InfrahubDatabase, user_id: str = SYSTEM_USER_ID) -> None:
397
+ async def rebase(
398
+ self, db: InfrahubDatabase, at: Optional[Union[str, Timestamp]] = None, user_id: str = SYSTEM_USER_ID
399
+ ) -> None:
506
400
  """Rebase the current Branch with its origin branch"""
507
401
 
508
- at = Timestamp()
402
+ at = Timestamp(at)
509
403
 
510
404
  await self.rebase_graph(db=db, at=at)
511
405
 
@@ -88,7 +88,7 @@ async def migrate_branch(branch: str, context: InfrahubContext, send_events: boo
88
88
 
89
89
  try:
90
90
  log.info(f"Running migrations for branch '{obj.name}'")
91
- await migration_runner.run(db=db)
91
+ await migration_runner.run(db=db, at=Timestamp())
92
92
  except MigrationFailureError as exc:
93
93
  log.error(f"Failed to run migrations for branch '{obj.name}': {exc.errors}")
94
94
  raise
@@ -170,7 +170,8 @@ async def rebase_branch(branch: str, context: InfrahubContext, send_events: bool
170
170
  migrations = []
171
171
  async with lock.registry.global_graph_lock():
172
172
  async with db.start_transaction() as dbt:
173
- await obj.rebase(db=dbt, user_id=context.account.account_id)
173
+ rebase_at = Timestamp()
174
+ await obj.rebase(db=dbt, user_id=context.account.account_id, at=rebase_at)
174
175
  log.info("Branch successfully rebased")
175
176
 
176
177
  if obj.has_schema_changes:
@@ -199,6 +200,7 @@ async def rebase_branch(branch: str, context: InfrahubContext, send_events: bool
199
200
  previous_schema=schema_in_main_before,
200
201
  migrations=migrations,
201
202
  user_id=context.account.account_id,
203
+ at=rebase_at,
202
204
  )
203
205
  )
204
206
  for error in errors:
@@ -291,7 +293,8 @@ async def merge_branch(branch: str, context: InfrahubContext, proposed_change_id
291
293
  diff_locker=DiffLocker(),
292
294
  workflow=get_workflow(),
293
295
  )
294
- branch_diff = await merger.merge()
296
+ merge_at = Timestamp()
297
+ branch_diff = await merger.merge(at=merge_at)
295
298
  await merger.update_schema()
296
299
 
297
300
  changelog_collector = DiffChangelogCollector(diff=branch_diff, branch=obj, db=db)
@@ -304,6 +307,7 @@ async def merge_branch(branch: str, context: InfrahubContext, proposed_change_id
304
307
  previous_schema=merger.initial_source_schema,
305
308
  migrations=merger.migrations,
306
309
  user_id=context.account.account_id,
310
+ at=merge_at,
307
311
  )
308
312
  )
309
313
  for error in errors:
@@ -41,7 +41,7 @@ class BranchDiffer:
41
41
  diff_to: str | Timestamp | None = None,
42
42
  db: InfrahubDatabase | None = None,
43
43
  service: InfrahubServices | None = None,
44
- ):
44
+ ) -> None:
45
45
  """_summary_
46
46
 
47
47
  Args:
@@ -9,7 +9,7 @@ from .model.path import (
9
9
 
10
10
 
11
11
  class DiffConflictTransferer:
12
- def __init__(self, diff_combiner: DiffCombiner):
12
+ def __init__(self, diff_combiner: DiffCombiner) -> None:
13
13
  self.diff_combiner = diff_combiner
14
14
 
15
15
  async def transfer(self, earlier: EnrichedDiffRoot, later: EnrichedDiffRoot) -> None:
@@ -29,7 +29,7 @@ class DiffDataCheckSynchronizer:
29
29
  conflicts_extractor: DiffConflictsExtractor,
30
30
  conflict_recorder: ObjectConflictValidatorRecorder,
31
31
  diff_repository: DiffRepository,
32
- ):
32
+ ) -> None:
33
33
  self.db = db
34
34
  self.conflicts_extractor = conflicts_extractor
35
35
  self.conflict_recorder = conflict_recorder
@@ -31,7 +31,7 @@ class DiffCardinalityOneEnricher(DiffEnricherInterface):
31
31
  - changes to properties (IS_PROTECTED, etc) of a cardinality=one relationship are consolidated as well
32
32
  """
33
33
 
34
- def __init__(self, db: InfrahubDatabase):
34
+ def __init__(self, db: InfrahubDatabase) -> None:
35
35
  self.db = db
36
36
  self._node_schema_map: dict[str, MainSchemaTypes] = {}
37
37
 
@@ -24,7 +24,7 @@ log = get_logger()
24
24
  class DiffHierarchyEnricher(DiffEnricherInterface):
25
25
  """Add hierarchy and parent/component nodes to diff even if the higher-level nodes are unchanged"""
26
26
 
27
- def __init__(self, db: InfrahubDatabase, parent_adder: DiffParentNodeAdder):
27
+ def __init__(self, db: InfrahubDatabase, parent_adder: DiffParentNodeAdder) -> None:
28
28
  self.db = db
29
29
  self.parent_adder = parent_adder
30
30
 
@@ -35,7 +35,7 @@ class DisplayLabelRequest:
35
35
  class DiffLabelsEnricher(DiffEnricherInterface):
36
36
  """Add display labels for nodes and labels for relationships"""
37
37
 
38
- def __init__(self, db: InfrahubDatabase):
38
+ def __init__(self, db: InfrahubDatabase) -> None:
39
39
  self.db = db
40
40
  self._base_branch_name: str | None = None
41
41
  self._diff_branch_name: str | None = None
@@ -36,7 +36,7 @@ class DiffMerger:
36
36
  destination_branch: Branch,
37
37
  diff_repository: DiffRepository,
38
38
  serializer: DiffMergeSerializer,
39
- ):
39
+ ) -> None:
40
40
  self.source_branch = source_branch
41
41
  self.destination_branch = destination_branch
42
42
  self.db = db
@@ -125,7 +125,11 @@ class DiffMerger:
125
125
  )
126
126
  await metadata_query.execute(db=self.db)
127
127
 
128
- self.source_branch.branched_from = at.to_string()
128
+ # set the branched_from time to the previous microsecond to prevent duplicated
129
+ # relationships on the branch after the merge
130
+ branched_from = at.subtract(microseconds=1)
131
+
132
+ self.source_branch.branched_from = branched_from.to_string()
129
133
  await self.source_branch.save(db=self.db)
130
134
  registry.branch[self.source_branch.name] = self.source_branch
131
135
  return enriched_diff
@@ -49,7 +49,9 @@ log = get_logger()
49
49
 
50
50
 
51
51
  class DiffRepository:
52
- def __init__(self, db: InfrahubDatabase, deserializer: EnrichedDiffDeserializer, max_save_batch_size: int = 1000):
52
+ def __init__(
53
+ self, db: InfrahubDatabase, deserializer: EnrichedDiffDeserializer, max_save_batch_size: int = 1000
54
+ ) -> None:
53
55
  self.db = db
54
56
  self.deserializer = deserializer
55
57
  self.max_save_batch_size = max_save_batch_size
@@ -1 +1 @@
1
- GRAPH_VERSION = 50
1
+ GRAPH_VERSION = 51
@@ -165,7 +165,7 @@ class ConstraintManagerBase:
165
165
  constraint_node_class: Optional[type[ConstraintItem]] = ConstraintItem
166
166
  constraint_rel_class: Optional[type[ConstraintItem]] = ConstraintItem
167
167
 
168
- def __init__(self, db: InfrahubDatabase):
168
+ def __init__(self, db: InfrahubDatabase) -> None:
169
169
  self.db = db
170
170
 
171
171
  self.nodes: list[ConstraintItem] = []
@@ -30,6 +30,7 @@ from infrahub.core.protocols import CoreAccount, CoreAccountGroup, CoreAccountRo
30
30
  from infrahub.core.root import Root
31
31
  from infrahub.core.schema import SchemaRoot, core_models, internal_schema
32
32
  from infrahub.core.schema.manager import SchemaManager
33
+ from infrahub.core.timestamp import Timestamp
33
34
  from infrahub.database import InfrahubDatabase
34
35
  from infrahub.database.memgraph import IndexManagerMemgraph
35
36
  from infrahub.database.neo4j import IndexManagerNeo4j
@@ -527,7 +528,7 @@ async def first_time_initialization(db: InfrahubDatabase) -> None:
527
528
  schema_branch = registry.schema.register_schema(schema=schema, branch=default_branch.name)
528
529
  schema_branch.load_schema(schema=SchemaRoot(**core_models))
529
530
  schema_branch.process()
530
- await registry.schema.load_schema_to_db(schema=schema_branch, branch=default_branch, db=db)
531
+ await registry.schema.load_schema_to_db(schema=schema_branch, branch=default_branch, db=db, at=Timestamp())
531
532
  registry.schema.set_schema_branch(name=default_branch.name, schema=schema_branch)
532
533
  default_branch.update_schema_hash()
533
534
  await default_branch.save(db=db)
@@ -101,16 +101,18 @@ class IpamReconciler:
101
101
  )
102
102
  await query.execute(db=self.db)
103
103
 
104
- ip_node_uuid = query.get_ip_node_uuid()
105
- if not ip_node_uuid:
104
+ data = query.get_data()
105
+ if not data or not data.ip_node_uuid:
106
106
  node_type = InfrahubKind.IPPREFIX
107
107
  if isinstance(ip_value, ipaddress.IPv6Interface | ipaddress.IPv4Interface):
108
108
  node_type = InfrahubKind.IPADDRESS
109
109
  raise NodeNotFoundError(node_type=node_type, identifier=str(ip_value))
110
- current_parent_uuid = query.get_current_parent_uuid()
111
- calculated_parent_uuid = query.get_calculated_parent_uuid()
112
- current_children_uuids = set(query.get_current_children_uuids())
113
- calculated_children_uuids = set(query.get_calculated_children_uuids())
110
+
111
+ ip_node_uuid = data.ip_node_uuid
112
+ current_parent_uuid = data.current_parent_uuid
113
+ calculated_parent_uuid = data.calculated_parent_uuid
114
+ current_children_uuids = set(data.current_children_uuids)
115
+ calculated_children_uuids = set(data.calculated_children_uuids)
114
116
 
115
117
  all_uuids: set[str] = set()
116
118
  all_uuids = (all_uuids | {ip_node_uuid}) if ip_node_uuid else all_uuids