infrahub-server 1.3.7__py3-none-any.whl → 1.4.0__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 (174) hide show
  1. infrahub/api/internal.py +5 -0
  2. infrahub/artifacts/tasks.py +17 -22
  3. infrahub/branch/merge_mutation_checker.py +38 -0
  4. infrahub/cli/__init__.py +2 -2
  5. infrahub/cli/context.py +7 -3
  6. infrahub/cli/db.py +5 -16
  7. infrahub/cli/upgrade.py +10 -29
  8. infrahub/computed_attribute/tasks.py +36 -46
  9. infrahub/config.py +57 -6
  10. infrahub/constants/environment.py +1 -0
  11. infrahub/core/attribute.py +15 -7
  12. infrahub/core/branch/tasks.py +43 -41
  13. infrahub/core/constants/__init__.py +21 -6
  14. infrahub/core/constants/infrahubkind.py +2 -0
  15. infrahub/core/diff/coordinator.py +3 -1
  16. infrahub/core/diff/model/path.py +0 -39
  17. infrahub/core/diff/repository/repository.py +0 -8
  18. infrahub/core/diff/tasks.py +11 -8
  19. infrahub/core/graph/__init__.py +1 -1
  20. infrahub/core/graph/index.py +1 -2
  21. infrahub/core/graph/schema.py +50 -29
  22. infrahub/core/initialization.py +81 -47
  23. infrahub/core/ipam/tasks.py +4 -3
  24. infrahub/core/merge.py +8 -10
  25. infrahub/core/migrations/__init__.py +2 -0
  26. infrahub/core/migrations/graph/__init__.py +4 -0
  27. infrahub/core/migrations/graph/m036_drop_attr_value_index.py +45 -0
  28. infrahub/core/migrations/graph/m037_index_attr_vals.py +577 -0
  29. infrahub/core/migrations/query/attribute_add.py +27 -2
  30. infrahub/core/migrations/schema/attribute_kind_update.py +156 -0
  31. infrahub/core/migrations/schema/tasks.py +6 -5
  32. infrahub/core/models.py +5 -1
  33. infrahub/core/node/proposed_change.py +43 -0
  34. infrahub/core/protocols.py +12 -0
  35. infrahub/core/query/attribute.py +32 -14
  36. infrahub/core/query/diff.py +11 -0
  37. infrahub/core/query/ipam.py +13 -7
  38. infrahub/core/query/node.py +51 -10
  39. infrahub/core/query/resource_manager.py +3 -3
  40. infrahub/core/schema/basenode_schema.py +8 -0
  41. infrahub/core/schema/definitions/core/__init__.py +10 -1
  42. infrahub/core/schema/definitions/core/ipam.py +28 -2
  43. infrahub/core/schema/definitions/core/propose_change.py +15 -0
  44. infrahub/core/schema/definitions/core/webhook.py +3 -0
  45. infrahub/core/schema/definitions/internal.py +1 -1
  46. infrahub/core/schema/generated/attribute_schema.py +1 -1
  47. infrahub/core/schema/generic_schema.py +10 -0
  48. infrahub/core/schema/manager.py +10 -1
  49. infrahub/core/schema/node_schema.py +22 -22
  50. infrahub/core/schema/profile_schema.py +8 -0
  51. infrahub/core/schema/schema_branch.py +11 -7
  52. infrahub/core/schema/template_schema.py +8 -0
  53. infrahub/core/validators/attribute/kind.py +5 -1
  54. infrahub/core/validators/checks_runner.py +5 -5
  55. infrahub/core/validators/determiner.py +22 -2
  56. infrahub/core/validators/tasks.py +6 -7
  57. infrahub/core/validators/uniqueness/checker.py +4 -2
  58. infrahub/core/validators/uniqueness/model.py +1 -0
  59. infrahub/core/validators/uniqueness/query.py +57 -7
  60. infrahub/database/__init__.py +2 -1
  61. infrahub/events/__init__.py +20 -0
  62. infrahub/events/constants.py +7 -0
  63. infrahub/events/generator.py +29 -2
  64. infrahub/events/proposed_change_action.py +203 -0
  65. infrahub/generators/tasks.py +24 -20
  66. infrahub/git/base.py +4 -7
  67. infrahub/git/integrator.py +21 -12
  68. infrahub/git/repository.py +15 -30
  69. infrahub/git/tasks.py +121 -106
  70. infrahub/graphql/app.py +2 -1
  71. infrahub/graphql/field_extractor.py +69 -0
  72. infrahub/graphql/manager.py +15 -11
  73. infrahub/graphql/mutations/account.py +2 -2
  74. infrahub/graphql/mutations/action.py +8 -2
  75. infrahub/graphql/mutations/artifact_definition.py +4 -1
  76. infrahub/graphql/mutations/branch.py +10 -5
  77. infrahub/graphql/mutations/graphql_query.py +2 -1
  78. infrahub/graphql/mutations/main.py +14 -8
  79. infrahub/graphql/mutations/menu.py +2 -1
  80. infrahub/graphql/mutations/proposed_change.py +230 -8
  81. infrahub/graphql/mutations/relationship.py +5 -0
  82. infrahub/graphql/mutations/repository.py +2 -1
  83. infrahub/graphql/mutations/tasks.py +7 -9
  84. infrahub/graphql/mutations/webhook.py +4 -1
  85. infrahub/graphql/parser.py +15 -6
  86. infrahub/graphql/queries/__init__.py +10 -1
  87. infrahub/graphql/queries/account.py +3 -3
  88. infrahub/graphql/queries/branch.py +2 -2
  89. infrahub/graphql/queries/diff/tree.py +56 -5
  90. infrahub/graphql/queries/event.py +13 -3
  91. infrahub/graphql/queries/ipam.py +23 -1
  92. infrahub/graphql/queries/proposed_change.py +84 -0
  93. infrahub/graphql/queries/relationship.py +2 -2
  94. infrahub/graphql/queries/resource_manager.py +3 -3
  95. infrahub/graphql/queries/search.py +3 -2
  96. infrahub/graphql/queries/status.py +3 -2
  97. infrahub/graphql/queries/task.py +2 -2
  98. infrahub/graphql/resolvers/ipam.py +440 -0
  99. infrahub/graphql/resolvers/many_relationship.py +4 -3
  100. infrahub/graphql/resolvers/resolver.py +5 -5
  101. infrahub/graphql/resolvers/single_relationship.py +3 -2
  102. infrahub/graphql/schema.py +25 -5
  103. infrahub/graphql/types/__init__.py +2 -2
  104. infrahub/graphql/types/attribute.py +3 -3
  105. infrahub/graphql/types/event.py +68 -0
  106. infrahub/groups/tasks.py +6 -6
  107. infrahub/lock.py +3 -2
  108. infrahub/menu/generator.py +8 -0
  109. infrahub/message_bus/operations/__init__.py +9 -12
  110. infrahub/message_bus/operations/git/file.py +6 -5
  111. infrahub/message_bus/operations/git/repository.py +12 -20
  112. infrahub/message_bus/operations/refresh/registry.py +15 -9
  113. infrahub/message_bus/operations/send/echo.py +7 -4
  114. infrahub/message_bus/types.py +1 -0
  115. infrahub/permissions/__init__.py +2 -1
  116. infrahub/permissions/constants.py +13 -0
  117. infrahub/permissions/globals.py +31 -2
  118. infrahub/permissions/manager.py +8 -5
  119. infrahub/pools/prefix.py +7 -5
  120. infrahub/prefect_server/app.py +31 -0
  121. infrahub/prefect_server/bootstrap.py +18 -0
  122. infrahub/proposed_change/action_checker.py +206 -0
  123. infrahub/proposed_change/approval_revoker.py +40 -0
  124. infrahub/proposed_change/branch_diff.py +3 -1
  125. infrahub/proposed_change/checker.py +45 -0
  126. infrahub/proposed_change/constants.py +32 -2
  127. infrahub/proposed_change/tasks.py +182 -150
  128. infrahub/py.typed +0 -0
  129. infrahub/server.py +29 -17
  130. infrahub/services/__init__.py +13 -28
  131. infrahub/services/adapters/cache/__init__.py +4 -0
  132. infrahub/services/adapters/cache/nats.py +2 -0
  133. infrahub/services/adapters/cache/redis.py +3 -0
  134. infrahub/services/adapters/message_bus/__init__.py +0 -2
  135. infrahub/services/adapters/message_bus/local.py +1 -2
  136. infrahub/services/adapters/message_bus/nats.py +6 -8
  137. infrahub/services/adapters/message_bus/rabbitmq.py +7 -9
  138. infrahub/services/adapters/workflow/__init__.py +1 -0
  139. infrahub/services/adapters/workflow/local.py +1 -8
  140. infrahub/services/component.py +2 -1
  141. infrahub/task_manager/event.py +56 -0
  142. infrahub/task_manager/models.py +9 -0
  143. infrahub/tasks/artifact.py +6 -7
  144. infrahub/tasks/check.py +4 -7
  145. infrahub/telemetry/tasks.py +15 -18
  146. infrahub/transformations/tasks.py +10 -6
  147. infrahub/trigger/tasks.py +4 -3
  148. infrahub/types.py +4 -0
  149. infrahub/validators/events.py +7 -7
  150. infrahub/validators/tasks.py +6 -7
  151. infrahub/webhook/models.py +45 -45
  152. infrahub/webhook/tasks.py +25 -24
  153. infrahub/workers/dependencies.py +143 -0
  154. infrahub/workers/infrahub_async.py +19 -43
  155. infrahub/workflows/catalogue.py +16 -2
  156. infrahub/workflows/initialization.py +5 -4
  157. infrahub/workflows/models.py +2 -0
  158. infrahub_sdk/client.py +2 -2
  159. infrahub_sdk/ctl/repository.py +51 -0
  160. infrahub_sdk/ctl/schema.py +9 -9
  161. infrahub_sdk/node/node.py +2 -2
  162. infrahub_sdk/pytest_plugin/items/graphql_query.py +1 -1
  163. infrahub_sdk/schema/repository.py +1 -1
  164. infrahub_sdk/testing/docker.py +1 -1
  165. infrahub_sdk/utils.py +2 -2
  166. {infrahub_server-1.3.7.dist-info → infrahub_server-1.4.0.dist-info}/METADATA +7 -5
  167. {infrahub_server-1.3.7.dist-info → infrahub_server-1.4.0.dist-info}/RECORD +174 -158
  168. infrahub_testcontainers/container.py +17 -0
  169. infrahub_testcontainers/docker-compose-cluster.test.yml +56 -1
  170. infrahub_testcontainers/docker-compose.test.yml +56 -1
  171. infrahub_testcontainers/helpers.py +4 -1
  172. {infrahub_server-1.3.7.dist-info → infrahub_server-1.4.0.dist-info}/LICENSE.txt +0 -0
  173. {infrahub_server-1.3.7.dist-info → infrahub_server-1.4.0.dist-info}/WHEEL +0 -0
  174. {infrahub_server-1.3.7.dist-info → infrahub_server-1.4.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,206 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING
6
+
7
+ from infrahub.core.account import GlobalPermission
8
+ from infrahub.core.constants import GlobalPermissions, PermissionDecision
9
+ from infrahub.exceptions import ValidationError
10
+
11
+ from .checker import verify_proposed_change_is_mergeable
12
+ from .constants import ProposedChangeAction, ProposedChangeState
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Sequence
16
+
17
+ from infrahub.core.protocols import CoreGenericAccount, CoreProposedChange
18
+ from infrahub.graphql.initialization import GraphqlContext
19
+
20
+
21
+ class Check(ABC):
22
+ @abstractmethod
23
+ async def evaluate(
24
+ self,
25
+ proposed_change: CoreProposedChange,
26
+ proposed_change_author: CoreGenericAccount,
27
+ graphql_context: GraphqlContext,
28
+ ) -> None: ...
29
+
30
+
31
+ class IsAuthor(Check):
32
+ async def evaluate(
33
+ self,
34
+ proposed_change: CoreProposedChange, # noqa: ARG002
35
+ proposed_change_author: CoreGenericAccount,
36
+ graphql_context: GraphqlContext,
37
+ ) -> None:
38
+ if proposed_change_author.id != graphql_context.active_account_session.account_id:
39
+ raise ValidationError("You are not the author of the proposed change")
40
+
41
+
42
+ class StateIs(Check):
43
+ def __init__(self, expected: Sequence[ProposedChangeState]) -> None:
44
+ self.expected = expected
45
+
46
+ async def evaluate(
47
+ self,
48
+ proposed_change: CoreProposedChange,
49
+ proposed_change_author: CoreGenericAccount, # noqa: ARG002
50
+ graphql_context: GraphqlContext, # noqa: ARG002
51
+ ) -> None:
52
+ if proposed_change.state.value.value not in self.expected:
53
+ raise ValidationError(f"The proposed change is not {', '.join([i.value for i in self.expected])}")
54
+
55
+
56
+ class DraftIs(Check):
57
+ def __init__(self, expected: bool) -> None:
58
+ self.expected = expected
59
+
60
+ async def evaluate(
61
+ self,
62
+ proposed_change: CoreProposedChange,
63
+ proposed_change_author: CoreGenericAccount, # noqa: ARG002
64
+ graphql_context: GraphqlContext, # noqa: ARG002
65
+ ) -> None:
66
+ if proposed_change.is_draft.value != self.expected:
67
+ if self.expected:
68
+ raise ValidationError("The proposed change is not a draft")
69
+ raise ValidationError("The proposed change is a draft")
70
+
71
+
72
+ class HasPermission(Check):
73
+ def __init__(self, permission: GlobalPermission) -> None:
74
+ self.permission = permission
75
+
76
+ async def evaluate(
77
+ self,
78
+ proposed_change: CoreProposedChange, # noqa: ARG002
79
+ proposed_change_author: CoreGenericAccount, # noqa: ARG002
80
+ graphql_context: GraphqlContext,
81
+ ) -> None:
82
+ if not graphql_context.active_permissions.has_permission(permission=self.permission):
83
+ raise ValidationError("You do not have the permission to perform this action")
84
+
85
+
86
+ class IsMergeable(Check):
87
+ async def evaluate(
88
+ self,
89
+ proposed_change: CoreProposedChange,
90
+ proposed_change_author: CoreGenericAccount, # noqa: ARG002
91
+ graphql_context: GraphqlContext,
92
+ ) -> None:
93
+ try:
94
+ await verify_proposed_change_is_mergeable(
95
+ proposed_change=proposed_change, # type: ignore[arg-type]
96
+ db=graphql_context.db,
97
+ account_session=graphql_context.active_account_session,
98
+ )
99
+ except ValueError as exc:
100
+ raise ValidationError(str(exc)) from exc
101
+
102
+
103
+ @dataclass
104
+ class ActionRule:
105
+ action: ProposedChangeAction
106
+ checks: list[Check]
107
+
108
+ async def evaluate(
109
+ self,
110
+ proposed_change: CoreProposedChange,
111
+ proposed_change_author: CoreGenericAccount,
112
+ graphql_context: GraphqlContext,
113
+ ) -> dict[str, str | bool | None]:
114
+ for check in self.checks:
115
+ try:
116
+ await check.evaluate(
117
+ proposed_change=proposed_change,
118
+ proposed_change_author=proposed_change_author,
119
+ graphql_context=graphql_context,
120
+ )
121
+ except ValidationError as exc:
122
+ return {"action": self.action.value, "available": False, "unavailability_reason": exc.message}
123
+
124
+ return {"action": self.action.value, "available": True, "unavailability_reason": None}
125
+
126
+
127
+ class ActionRulesEvaluator:
128
+ def __init__(self, rules: list[ActionRule]):
129
+ self.rules = rules
130
+
131
+ async def evaluate(
132
+ self,
133
+ proposed_change: CoreProposedChange,
134
+ proposed_change_author: CoreGenericAccount,
135
+ graphql_context: GraphqlContext,
136
+ ) -> list[dict[str, str | bool | None]]:
137
+ report: list[dict[str, str | bool | None]] = []
138
+ for rule in self.rules:
139
+ report.append(
140
+ await rule.evaluate(
141
+ proposed_change=proposed_change,
142
+ proposed_change_author=proposed_change_author,
143
+ graphql_context=graphql_context,
144
+ )
145
+ )
146
+ return report
147
+
148
+
149
+ MERGE_PROPOSED_CHANGE_PERMISSION = GlobalPermission(
150
+ action=GlobalPermissions.MERGE_PROPOSED_CHANGE.value,
151
+ decision=PermissionDecision.ALLOW_ALL.value,
152
+ )
153
+ REVIEW_PROPOSED_CHANGE_PERMISSION = GlobalPermission(
154
+ action=GlobalPermissions.REVIEW_PROPOSED_CHANGE.value,
155
+ decision=PermissionDecision.ALLOW_ALL.value,
156
+ )
157
+
158
+ ACTION_RULES = [
159
+ ActionRule(action=ProposedChangeAction.OPEN, checks=[StateIs(expected=[ProposedChangeState.CLOSED])]),
160
+ ActionRule(action=ProposedChangeAction.CLOSE, checks=[StateIs(expected=[ProposedChangeState.OPEN])]),
161
+ ActionRule(
162
+ action=ProposedChangeAction.SET_DRAFT,
163
+ checks=[IsAuthor(), StateIs(expected=[ProposedChangeState.OPEN]), DraftIs(expected=False)],
164
+ ),
165
+ ActionRule(
166
+ action=ProposedChangeAction.UNSET_DRAFT,
167
+ checks=[IsAuthor(), StateIs(expected=[ProposedChangeState.OPEN]), DraftIs(expected=True)],
168
+ ),
169
+ ActionRule(
170
+ action=ProposedChangeAction.APPROVE,
171
+ checks=[
172
+ StateIs(expected=[ProposedChangeState.OPEN]),
173
+ HasPermission(permission=REVIEW_PROPOSED_CHANGE_PERMISSION),
174
+ ],
175
+ ),
176
+ ActionRule(
177
+ action=ProposedChangeAction.CANCEL_APPROVE,
178
+ checks=[
179
+ StateIs(expected=[ProposedChangeState.OPEN]),
180
+ HasPermission(permission=REVIEW_PROPOSED_CHANGE_PERMISSION),
181
+ ],
182
+ ),
183
+ ActionRule(
184
+ action=ProposedChangeAction.REJECT,
185
+ checks=[
186
+ StateIs(expected=[ProposedChangeState.OPEN]),
187
+ HasPermission(permission=REVIEW_PROPOSED_CHANGE_PERMISSION),
188
+ ],
189
+ ),
190
+ ActionRule(
191
+ action=ProposedChangeAction.CANCEL_REJECT,
192
+ checks=[
193
+ StateIs(expected=[ProposedChangeState.OPEN]),
194
+ HasPermission(permission=REVIEW_PROPOSED_CHANGE_PERMISSION),
195
+ ],
196
+ ),
197
+ ActionRule(
198
+ action=ProposedChangeAction.MERGE,
199
+ checks=[
200
+ StateIs(expected=[ProposedChangeState.OPEN]),
201
+ DraftIs(expected=False),
202
+ HasPermission(permission=MERGE_PROPOSED_CHANGE_PERMISSION),
203
+ IsMergeable(),
204
+ ],
205
+ ),
206
+ ]
@@ -0,0 +1,40 @@
1
+ import logging
2
+ from abc import ABC, abstractmethod
3
+
4
+ from fast_depends import Depends, inject
5
+
6
+ from infrahub.database import InfrahubDatabase
7
+
8
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ class ApprovalRevoker(ABC):
12
+ @abstractmethod
13
+ async def revoke_approvals_on_updated_pcs(
14
+ self,
15
+ db: InfrahubDatabase,
16
+ proposed_changes_ids: list[str] | None,
17
+ ) -> None:
18
+ raise NotImplementedError()
19
+
20
+
21
+ class ApprovalRevokerCommunity(ApprovalRevoker):
22
+ async def revoke_approvals_on_updated_pcs(
23
+ self,
24
+ db: InfrahubDatabase, # noqa: ARG002
25
+ proposed_changes_ids: list[str] | None, # noqa: ARG002
26
+ ) -> None:
27
+ raise ValueError("Revoking existing approvals based on branch changes is an enterprise feature.")
28
+
29
+
30
+ def get_approval_revoker() -> ApprovalRevoker:
31
+ return ApprovalRevokerCommunity()
32
+
33
+
34
+ @inject
35
+ async def do_revoke_approvals_on_updated_pcs(
36
+ db: InfrahubDatabase,
37
+ proposed_changes_ids: list[str] | None = None,
38
+ approval_revoker: ApprovalRevoker = Depends(get_approval_revoker), # noqa: B008
39
+ ) -> None:
40
+ await approval_revoker.revoke_approvals_on_updated_pcs(db=db, proposed_changes_ids=proposed_changes_ids)
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, cast
6
6
 
7
7
  from infrahub.exceptions import ResourceNotFoundError
8
8
  from infrahub.message_bus.types import KVTTL
9
+ from infrahub.workers.dependencies import get_cache
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from uuid import UUID
@@ -54,7 +55,8 @@ async def set_diff_summary_cache(pipeline_id: UUID, diff_summary: list[NodeDiff]
54
55
  )
55
56
 
56
57
 
57
- async def get_diff_summary_cache(pipeline_id: UUID, cache: InfrahubCache) -> list[NodeDiff]:
58
+ async def get_diff_summary_cache(pipeline_id: UUID) -> list[NodeDiff]:
59
+ cache = await get_cache()
58
60
  summary_payload = await cache.get(
59
61
  key=f"proposed_change:pipeline:pipeline_id:{pipeline_id}:diff_summary",
60
62
  )
@@ -0,0 +1,45 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from fast_depends import Depends, inject
4
+
5
+ from infrahub.auth import AccountSession
6
+ from infrahub.core.node import Node
7
+ from infrahub.database import InfrahubDatabase
8
+
9
+
10
+ class ProposedChangeChecker(ABC):
11
+ # We can't use CoreProposedChange type instead of Node as fast_depends enforces pydantic runtime type checks.
12
+ @abstractmethod
13
+ async def verify_proposed_change_is_mergeable(
14
+ self, proposed_change: Node, db: InfrahubDatabase, account_session: AccountSession
15
+ ) -> None:
16
+ """
17
+ Raise an error if proposed change cannot be merged.
18
+ """
19
+
20
+ raise NotImplementedError()
21
+
22
+
23
+ class ProposedChangeCheckerCommunity(ProposedChangeChecker):
24
+ async def verify_proposed_change_is_mergeable(
25
+ self, proposed_change: Node, db: InfrahubDatabase, account_session: AccountSession
26
+ ) -> None:
27
+ pass
28
+
29
+
30
+ def get_proposed_change_checker() -> ProposedChangeChecker:
31
+ return ProposedChangeCheckerCommunity()
32
+
33
+
34
+ # We can't use CoreProposedChange type instead of Node as fast_depends enforces pydantic runtime type checks.
35
+ @inject
36
+ async def verify_proposed_change_is_mergeable(
37
+ proposed_change: Node,
38
+ db: InfrahubDatabase,
39
+ account_session: AccountSession,
40
+ pc_checker: ProposedChangeChecker = Depends(get_proposed_change_checker), # noqa: B008
41
+ ) -> None:
42
+ # type ignore due to fast-depends enforcing pydantic checks
43
+ await pc_checker.verify_proposed_change_is_mergeable(
44
+ proposed_change=proposed_change, db=db, account_session=account_session
45
+ )
@@ -4,6 +4,13 @@ from infrahub.exceptions import ValidationError
4
4
  from infrahub.utils import InfrahubStringEnum
5
5
 
6
6
 
7
+ class ProposedChangeApprovalDecision(InfrahubStringEnum):
8
+ APPROVE = "approve"
9
+ CANCEL_APPROVE = "cancel-approve"
10
+ REJECT = "reject"
11
+ CANCEL_REJECT = "cancel-reject"
12
+
13
+
7
14
  class ProposedChangeState(InfrahubStringEnum):
8
15
  OPEN = "open"
9
16
  MERGED = "merged"
@@ -11,18 +18,29 @@ class ProposedChangeState(InfrahubStringEnum):
11
18
  CLOSED = "closed"
12
19
  CANCELED = "canceled"
13
20
 
21
+ @property
22
+ def is_completed(self) -> bool:
23
+ """Check if the proposed change is in a completed state."""
24
+ return self != ProposedChangeState.OPEN
25
+
14
26
  def validate_state_check_run(self) -> None:
15
27
  if self == ProposedChangeState.OPEN:
16
28
  return
17
29
 
18
30
  raise ValidationError(input_value="Unable to trigger check on proposed changes that aren't in the open state")
19
31
 
20
- def validate_editability(self) -> None:
21
- if self in [ProposedChangeState.CANCELED, ProposedChangeState.MERGED, ProposedChangeState.MERGED]:
32
+ def validate_updatable(self) -> None:
33
+ if self.is_completed:
22
34
  raise ValidationError(
23
35
  input_value=f"A proposed change in the {self.value} state is not allowed to be updated"
24
36
  )
25
37
 
38
+ def validate_reviewable(self) -> None:
39
+ if self.is_completed:
40
+ raise ValidationError(
41
+ input_value=f"A proposed change in the {self.value} state is not allowed to be reviewed"
42
+ )
43
+
26
44
  def validate_state_transition(self, updated_state: ProposedChangeState) -> None:
27
45
  if self == ProposedChangeState.OPEN:
28
46
  return
@@ -34,3 +52,15 @@ class ProposedChangeState(InfrahubStringEnum):
34
52
  raise ValidationError(
35
53
  input_value="A closed proposed change is only allowed to transition to the open state"
36
54
  )
55
+
56
+
57
+ class ProposedChangeAction(InfrahubStringEnum):
58
+ OPEN = "open"
59
+ CLOSE = "close"
60
+ SET_DRAFT = "set-draft"
61
+ UNSET_DRAFT = "unset-draft"
62
+ MERGE = "merge"
63
+ APPROVE = "approve"
64
+ CANCEL_APPROVE = "cancel-approve"
65
+ REJECT = "reject"
66
+ CANCEL_REJECT = "cancel-reject"