We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
ProjectMergeRequestApprovalRule.save() throws 404
## Description of the problem, including code/CLI snippet
`gl.project.get(proj_id).merge_requests.get(mr_iid).approval_rules.get(rule_id).save()`

This example is an MVP example; actually making changes to the rule object before calling `.save()` doesn't change the behaviour

## Expected Behavior
The function should succeed silently, returning `None`

## Actual Behavior
`gitlab.exceptions.GitlabUpdateError: 404: 404 Not found` is thrown. Trying it with debug mode on, it appears as though the root cause of the issue is that when the CLI invokes `/projects/:id/merge_requests/:merge_request_iid/approval_rules/:approval_rule_id` in the API, `:id` (i.e. project ID) is passed where the URL expects `:approval_rule_id`, as can be seen from this debug output (anonymized to remove sensitive information)

```
>>> rule.save()
DEBUG:urllib3.connectionpool:Resetting dropped connection: mygitlab.example.com
send: b'PUT /api/v4/projects/93/merge_requests/1/approval_rules/93 HTTP/1.1\r\nHost: mygitlab.example.com\r\nUser-Agent: python-gitlab/3.14.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nPRIVATE-TOKEN: TOKEN\r\nContent-type: application/json\r\nContent-Length: 768\r\n\r\n'
send: b'{"id": "93", "merge_request_iid": "1", "approval_rule_id": 89, "name": "testrule", "approvals_required": 1, "users": ["{\\"id\\": 168, \\"username\\": \\"myusername\\", \\"name\\": \\"My Name\\", \\"state\\": \\"active\\", \\"avatar_url\\": \\"https://secure.gravatar.com/avatar/8306d9f17d1c91970c2447b61c7a9f29?s=80&d=identicon\\", \\"web_url\\": \\"https://mygitlab.example.com/myusername\\", \\"created_at\\": \\"2023-03-29T14:30:13.371Z\\", \\"bio\\": \\"\\", \\"location\\": null, \\"public_email\\": null, \\"skype\\": \\"\\", \\"linkedin\\": \\"\\", \\"twitter\\": \\"\\", \\"website_url\\": \\"\\", \\"organization\\": null, \\"job_title\\": \\"\\", \\"pronouns\\": null, \\"bot\\": false, \\"work_information\\": null, \\"followers\\": 0, \\"following\\": 0, \\"is_followed\\": false, \\"local_time\\": null}"]}'
reply: 'HTTP/1.1 404 Not Found\r\n'
```

## Specifications

  - python-gitlab version: 3.14.0
  - API version you are using (v3/v4): v4
  - Gitlab server version (or gitlab.com): 15.7.2-ee


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `gitlab/v4/objects/merge_request_approvals.py`
Content:
```
1 from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
2 
3 from gitlab import exceptions as exc
4 from gitlab.base import RESTManager, RESTObject
5 from gitlab.mixins import (
6     CreateMixin,
7     CRUDMixin,
8     DeleteMixin,
9     GetWithoutIdMixin,
10     ListMixin,
11     ObjectDeleteMixin,
12     SaveMixin,
13     UpdateMethod,
14     UpdateMixin,
15 )
16 from gitlab.types import RequiredOptional
17 
18 __all__ = [
19     "ProjectApproval",
20     "ProjectApprovalManager",
21     "ProjectApprovalRule",
22     "ProjectApprovalRuleManager",
23     "ProjectMergeRequestApproval",
24     "ProjectMergeRequestApprovalManager",
25     "ProjectMergeRequestApprovalRule",
26     "ProjectMergeRequestApprovalRuleManager",
27     "ProjectMergeRequestApprovalState",
28     "ProjectMergeRequestApprovalStateManager",
29 ]
30 
31 
32 class ProjectApproval(SaveMixin, RESTObject):
33     _id_attr = None
34 
35 
36 class ProjectApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
37     _path = "/projects/{project_id}/approvals"
38     _obj_cls = ProjectApproval
39     _from_parent_attrs = {"project_id": "id"}
40     _update_attrs = RequiredOptional(
41         optional=(
42             "approvals_before_merge",
43             "reset_approvals_on_push",
44             "disable_overriding_approvers_per_merge_request",
45             "merge_requests_author_approval",
46             "merge_requests_disable_committers_approval",
47         ),
48     )
49     _update_method = UpdateMethod.POST
50 
51     def get(self, **kwargs: Any) -> ProjectApproval:
52         return cast(ProjectApproval, super().get(**kwargs))
53 
54 
55 class ProjectApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject):
56     _id_attr = "id"
57 
58 
59 class ProjectApprovalRuleManager(
60     ListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager
61 ):
62     _path = "/projects/{project_id}/approval_rules"
63     _obj_cls = ProjectApprovalRule
64     _from_parent_attrs = {"project_id": "id"}
65     _create_attrs = RequiredOptional(
66         required=("name", "approvals_required"),
67         optional=("user_ids", "group_ids", "protected_branch_ids", "usernames"),
68     )
69 
70 
71 class ProjectMergeRequestApproval(SaveMixin, RESTObject):
72     _id_attr = None
73 
74 
75 class ProjectMergeRequestApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTManager):
76     _path = "/projects/{project_id}/merge_requests/{mr_iid}/approvals"
77     _obj_cls = ProjectMergeRequestApproval
78     _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"}
79     _update_attrs = RequiredOptional(required=("approvals_required",))
80     _update_method = UpdateMethod.POST
81 
82     def get(self, **kwargs: Any) -> ProjectMergeRequestApproval:
83         return cast(ProjectMergeRequestApproval, super().get(**kwargs))
84 
85     @exc.on_http_error(exc.GitlabUpdateError)
86     def set_approvers(
87         self,
88         approvals_required: int,
89         approver_ids: Optional[List[int]] = None,
90         approver_group_ids: Optional[List[int]] = None,
91         approval_rule_name: str = "name",
92         **kwargs: Any,
93     ) -> RESTObject:
94         """Change MR-level allowed approvers and approver groups.
95 
96         Args:
97             approvals_required: The number of required approvals for this rule
98             approver_ids: User IDs that can approve MRs
99             approver_group_ids: Group IDs whose members can approve MRs
100 
101         Raises:
102             GitlabAuthenticationError: If authentication is not correct
103             GitlabUpdateError: If the server failed to perform the request
104         """
105         approver_ids = approver_ids or []
106         approver_group_ids = approver_group_ids or []
107 
108         data = {
109             "name": approval_rule_name,
110             "approvals_required": approvals_required,
111             "rule_type": "regular",
112             "user_ids": approver_ids,
113             "group_ids": approver_group_ids,
114         }
115         if TYPE_CHECKING:
116             assert self._parent is not None
117         approval_rules: ProjectMergeRequestApprovalRuleManager = (
118             self._parent.approval_rules
119         )
120         # update any existing approval rule matching the name
121         existing_approval_rules = approval_rules.list()
122         for ar in existing_approval_rules:
123             if ar.name == approval_rule_name:
124                 ar.user_ids = data["user_ids"]
125                 ar.approvals_required = data["approvals_required"]
126                 ar.group_ids = data["group_ids"]
127                 ar.save()
128                 return ar
129         # if there was no rule matching the rule name, create a new one
130         return approval_rules.create(data=data, **kwargs)
131 
132 
133 class ProjectMergeRequestApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject):
134     _repr_attr = "name"
135     id: int
136     approval_rule_id: int
137     merge_request_iid: int
138 
139     @exc.on_http_error(exc.GitlabUpdateError)
140     def save(self, **kwargs: Any) -> None:
141         """Save the changes made to the object to the server.
142 
143         The object is updated to match what the server returns.
144 
145         Args:
146             **kwargs: Extra options to send to the server (e.g. sudo)
147 
148         Raise:
149             GitlabAuthenticationError: If authentication is not correct
150             GitlabUpdateError: If the server cannot perform the request
151         """
152         # There is a mismatch between the name of our id attribute and the put
153         # REST API name for the project_id, so we override it here.
154         self.approval_rule_id = self.id
155         self.merge_request_iid = self._parent_attrs["mr_iid"]
156         self.id = self._parent_attrs["project_id"]
157         # save will update self.id with the result from the server, so no need
158         # to overwrite with what it was before we overwrote it.
159         SaveMixin.save(self, **kwargs)
160 
161 
162 class ProjectMergeRequestApprovalRuleManager(CRUDMixin, RESTManager):
163     _path = "/projects/{project_id}/merge_requests/{mr_iid}/approval_rules"
164     _obj_cls = ProjectMergeRequestApprovalRule
165     _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"}
166     _update_attrs = RequiredOptional(
167         required=(
168             "id",
169             "merge_request_iid",
170             "approval_rule_id",
171             "name",
172             "approvals_required",
173         ),
174         optional=("user_ids", "group_ids"),
175     )
176     # Important: When approval_project_rule_id is set, the name, users and
177     # groups of project-level rule will be copied. The approvals_required
178     # specified will be used.
179     _create_attrs = RequiredOptional(
180         required=("id", "merge_request_iid", "name", "approvals_required"),
181         optional=("approval_project_rule_id", "user_ids", "group_ids"),
182     )
183 
184     def get(
185         self, id: Union[str, int], lazy: bool = False, **kwargs: Any
186     ) -> ProjectMergeRequestApprovalRule:
187         return cast(
188             ProjectMergeRequestApprovalRule, super().get(id=id, lazy=lazy, **kwargs)
189         )
190 
191     def create(
192         self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
193     ) -> RESTObject:
194         """Create a new object.
195 
196         Args:
197             data: Parameters to send to the server to create the
198                          resource
199             **kwargs: Extra options to send to the server (e.g. sudo or
200                       'ref_name', 'stage', 'name', 'all')
201 
202         Raises:
203             GitlabAuthenticationError: If authentication is not correct
204             GitlabCreateError: If the server cannot perform the request
205 
206         Returns:
207             A new instance of the manage object class build with
208                 the data sent by the server
209         """
210         if TYPE_CHECKING:
211             assert data is not None
212         new_data = data.copy()
213         new_data["id"] = self._from_parent_attrs["project_id"]
214         new_data["merge_request_iid"] = self._from_parent_attrs["mr_iid"]
215         return CreateMixin.create(self, new_data, **kwargs)
216 
217 
218 class ProjectMergeRequestApprovalState(RESTObject):
219     pass
220 
221 
222 class ProjectMergeRequestApprovalStateManager(GetWithoutIdMixin, RESTManager):
223     _path = "/projects/{project_id}/merge_requests/{mr_iid}/approval_state"
224     _obj_cls = ProjectMergeRequestApprovalState
225     _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"}
226 
227     def get(self, **kwargs: Any) -> ProjectMergeRequestApprovalState:
228         return cast(ProjectMergeRequestApprovalState, super().get(**kwargs))
229
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/gitlab/v4/objects/merge_request_approvals.py b/gitlab/v4/objects/merge_request_approvals.py
--- a/gitlab/v4/objects/merge_request_approvals.py
+++ b/gitlab/v4/objects/merge_request_approvals.py
@@ -1,4 +1,4 @@
-from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
+from typing import Any, cast, List, Optional, TYPE_CHECKING, Union
 
 from gitlab import exceptions as exc
 from gitlab.base import RESTManager, RESTObject
@@ -132,42 +132,16 @@
 
 class ProjectMergeRequestApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject):
     _repr_attr = "name"
-    id: int
-    approval_rule_id: int
-    merge_request_iid: int
-
-    @exc.on_http_error(exc.GitlabUpdateError)
-    def save(self, **kwargs: Any) -> None:
-        """Save the changes made to the object to the server.
-
-        The object is updated to match what the server returns.
-
-        Args:
-            **kwargs: Extra options to send to the server (e.g. sudo)
-
-        Raise:
-            GitlabAuthenticationError: If authentication is not correct
-            GitlabUpdateError: If the server cannot perform the request
-        """
-        # There is a mismatch between the name of our id attribute and the put
-        # REST API name for the project_id, so we override it here.
-        self.approval_rule_id = self.id
-        self.merge_request_iid = self._parent_attrs["mr_iid"]
-        self.id = self._parent_attrs["project_id"]
-        # save will update self.id with the result from the server, so no need
-        # to overwrite with what it was before we overwrote it.
-        SaveMixin.save(self, **kwargs)
 
 
 class ProjectMergeRequestApprovalRuleManager(CRUDMixin, RESTManager):
-    _path = "/projects/{project_id}/merge_requests/{mr_iid}/approval_rules"
+    _path = "/projects/{project_id}/merge_requests/{merge_request_iid}/approval_rules"
     _obj_cls = ProjectMergeRequestApprovalRule
-    _from_parent_attrs = {"project_id": "project_id", "mr_iid": "iid"}
+    _from_parent_attrs = {"project_id": "project_id", "merge_request_iid": "iid"}
     _update_attrs = RequiredOptional(
         required=(
             "id",
             "merge_request_iid",
-            "approval_rule_id",
             "name",
             "approvals_required",
         ),
@@ -177,7 +151,7 @@
     # groups of project-level rule will be copied. The approvals_required
     # specified will be used.
     _create_attrs = RequiredOptional(
-        required=("id", "merge_request_iid", "name", "approvals_required"),
+        required=("name", "approvals_required"),
         optional=("approval_project_rule_id", "user_ids", "group_ids"),
     )
 
@@ -188,32 +162,6 @@
             ProjectMergeRequestApprovalRule, super().get(id=id, lazy=lazy, **kwargs)
         )
 
-    def create(
-        self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
-    ) -> RESTObject:
-        """Create a new object.
-
-        Args:
-            data: Parameters to send to the server to create the
-                         resource
-            **kwargs: Extra options to send to the server (e.g. sudo or
-                      'ref_name', 'stage', 'name', 'all')
-
-        Raises:
-            GitlabAuthenticationError: If authentication is not correct
-            GitlabCreateError: If the server cannot perform the request
-
-        Returns:
-            A new instance of the manage object class build with
-                the data sent by the server
-        """
-        if TYPE_CHECKING:
-            assert data is not None
-        new_data = data.copy()
-        new_data["id"] = self._from_parent_attrs["project_id"]
-        new_data["merge_request_iid"] = self._from_parent_attrs["mr_iid"]
-        return CreateMixin.create(self, new_data, **kwargs)
-
 
 class ProjectMergeRequestApprovalState(RESTObject):
     pass

{"golden_diff": "diff --git a/gitlab/v4/objects/merge_request_approvals.py b/gitlab/v4/objects/merge_request_approvals.py\n--- a/gitlab/v4/objects/merge_request_approvals.py\n+++ b/gitlab/v4/objects/merge_request_approvals.py\n@@ -1,4 +1,4 @@\n-from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union\n+from typing import Any, cast, List, Optional, TYPE_CHECKING, Union\n \n from gitlab import exceptions as exc\n from gitlab.base import RESTManager, RESTObject\n@@ -132,42 +132,16 @@\n \n class ProjectMergeRequestApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject):\n     _repr_attr = \"name\"\n-    id: int\n-    approval_rule_id: int\n-    merge_request_iid: int\n-\n-    @exc.on_http_error(exc.GitlabUpdateError)\n-    def save(self, **kwargs: Any) -> None:\n-        \"\"\"Save the changes made to the object to the server.\n-\n-        The object is updated to match what the server returns.\n-\n-        Args:\n-            **kwargs: Extra options to send to the server (e.g. sudo)\n-\n-        Raise:\n-            GitlabAuthenticationError: If authentication is not correct\n-            GitlabUpdateError: If the server cannot perform the request\n-        \"\"\"\n-        # There is a mismatch between the name of our id attribute and the put\n-        # REST API name for the project_id, so we override it here.\n-        self.approval_rule_id = self.id\n-        self.merge_request_iid = self._parent_attrs[\"mr_iid\"]\n-        self.id = self._parent_attrs[\"project_id\"]\n-        # save will update self.id with the result from the server, so no need\n-        # to overwrite with what it was before we overwrote it.\n-        SaveMixin.save(self, **kwargs)\n \n \n class ProjectMergeRequestApprovalRuleManager(CRUDMixin, RESTManager):\n-    _path = \"/projects/{project_id}/merge_requests/{mr_iid}/approval_rules\"\n+    _path = \"/projects/{project_id}/merge_requests/{merge_request_iid}/approval_rules\"\n     _obj_cls = ProjectMergeRequestApprovalRule\n-    _from_parent_attrs = {\"project_id\": \"project_id\", \"mr_iid\": \"iid\"}\n+    _from_parent_attrs = {\"project_id\": \"project_id\", \"merge_request_iid\": \"iid\"}\n     _update_attrs = RequiredOptional(\n         required=(\n             \"id\",\n             \"merge_request_iid\",\n-            \"approval_rule_id\",\n             \"name\",\n             \"approvals_required\",\n         ),\n@@ -177,7 +151,7 @@\n     # groups of project-level rule will be copied. The approvals_required\n     # specified will be used.\n     _create_attrs = RequiredOptional(\n-        required=(\"id\", \"merge_request_iid\", \"name\", \"approvals_required\"),\n+        required=(\"name\", \"approvals_required\"),\n         optional=(\"approval_project_rule_id\", \"user_ids\", \"group_ids\"),\n     )\n \n@@ -188,32 +162,6 @@\n             ProjectMergeRequestApprovalRule, super().get(id=id, lazy=lazy, **kwargs)\n         )\n \n-    def create(\n-        self, data: Optional[Dict[str, Any]] = None, **kwargs: Any\n-    ) -> RESTObject:\n-        \"\"\"Create a new object.\n-\n-        Args:\n-            data: Parameters to send to the server to create the\n-                         resource\n-            **kwargs: Extra options to send to the server (e.g. sudo or\n-                      'ref_name', 'stage', 'name', 'all')\n-\n-        Raises:\n-            GitlabAuthenticationError: If authentication is not correct\n-            GitlabCreateError: If the server cannot perform the request\n-\n-        Returns:\n-            A new instance of the manage object class build with\n-                the data sent by the server\n-        \"\"\"\n-        if TYPE_CHECKING:\n-            assert data is not None\n-        new_data = data.copy()\n-        new_data[\"id\"] = self._from_parent_attrs[\"project_id\"]\n-        new_data[\"merge_request_iid\"] = self._from_parent_attrs[\"mr_iid\"]\n-        return CreateMixin.create(self, new_data, **kwargs)\n-\n \n class ProjectMergeRequestApprovalState(RESTObject):\n     pass\n", "issue": "ProjectMergeRequestApprovalRule.save() throws 404\n## Description of the problem, including code/CLI snippet\r\n`gl.project.get(proj_id).merge_requests.get(mr_iid).approval_rules.get(rule_id).save()`\r\n\r\nThis example is an MVP example; actually making changes to the rule object before calling `.save()` doesn't change the behaviour\r\n\r\n## Expected Behavior\r\nThe function should succeed silently, returning `None`\r\n\r\n## Actual Behavior\r\n`gitlab.exceptions.GitlabUpdateError: 404: 404 Not found` is thrown. Trying it with debug mode on, it appears as though the root cause of the issue is that when the CLI invokes `/projects/:id/merge_requests/:merge_request_iid/approval_rules/:approval_rule_id` in the API, `:id` (i.e. project ID) is passed where the URL expects `:approval_rule_id`, as can be seen from this debug output (anonymized to remove sensitive information)\r\n\r\n```\r\n>>> rule.save()\r\nDEBUG:urllib3.connectionpool:Resetting dropped connection: mygitlab.example.com\r\nsend: b'PUT /api/v4/projects/93/merge_requests/1/approval_rules/93 HTTP/1.1\\r\\nHost: mygitlab.example.com\\r\\nUser-Agent: python-gitlab/3.14.0\\r\\nAccept-Encoding: gzip, deflate\\r\\nAccept: */*\\r\\nConnection: keep-alive\\r\\nPRIVATE-TOKEN: TOKEN\\r\\nContent-type: application/json\\r\\nContent-Length: 768\\r\\n\\r\\n'\r\nsend: b'{\"id\": \"93\", \"merge_request_iid\": \"1\", \"approval_rule_id\": 89, \"name\": \"testrule\", \"approvals_required\": 1, \"users\": [\"{\\\\\"id\\\\\": 168, \\\\\"username\\\\\": \\\\\"myusername\\\\\", \\\\\"name\\\\\": \\\\\"My Name\\\\\", \\\\\"state\\\\\": \\\\\"active\\\\\", \\\\\"avatar_url\\\\\": \\\\\"https://secure.gravatar.com/avatar/8306d9f17d1c91970c2447b61c7a9f29?s=80&d=identicon\\\\\", \\\\\"web_url\\\\\": \\\\\"https://mygitlab.example.com/myusername\\\\\", \\\\\"created_at\\\\\": \\\\\"2023-03-29T14:30:13.371Z\\\\\", \\\\\"bio\\\\\": \\\\\"\\\\\", \\\\\"location\\\\\": null, \\\\\"public_email\\\\\": null, \\\\\"skype\\\\\": \\\\\"\\\\\", \\\\\"linkedin\\\\\": \\\\\"\\\\\", \\\\\"twitter\\\\\": \\\\\"\\\\\", \\\\\"website_url\\\\\": \\\\\"\\\\\", \\\\\"organization\\\\\": null, \\\\\"job_title\\\\\": \\\\\"\\\\\", \\\\\"pronouns\\\\\": null, \\\\\"bot\\\\\": false, \\\\\"work_information\\\\\": null, \\\\\"followers\\\\\": 0, \\\\\"following\\\\\": 0, \\\\\"is_followed\\\\\": false, \\\\\"local_time\\\\\": null}\"]}'\r\nreply: 'HTTP/1.1 404 Not Found\\r\\n'\r\n```\r\n\r\n## Specifications\r\n\r\n  - python-gitlab version: 3.14.0\r\n  - API version you are using (v3/v4): v4\r\n  - Gitlab server version (or gitlab.com): 15.7.2-ee\r\n\n", "before_files": [{"content": "from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union\n\nfrom gitlab import exceptions as exc\nfrom gitlab.base import RESTManager, RESTObject\nfrom gitlab.mixins import (\n    CreateMixin,\n    CRUDMixin,\n    DeleteMixin,\n    GetWithoutIdMixin,\n    ListMixin,\n    ObjectDeleteMixin,\n    SaveMixin,\n    UpdateMethod,\n    UpdateMixin,\n)\nfrom gitlab.types import RequiredOptional\n\n__all__ = [\n    \"ProjectApproval\",\n    \"ProjectApprovalManager\",\n    \"ProjectApprovalRule\",\n    \"ProjectApprovalRuleManager\",\n    \"ProjectMergeRequestApproval\",\n    \"ProjectMergeRequestApprovalManager\",\n    \"ProjectMergeRequestApprovalRule\",\n    \"ProjectMergeRequestApprovalRuleManager\",\n    \"ProjectMergeRequestApprovalState\",\n    \"ProjectMergeRequestApprovalStateManager\",\n]\n\n\nclass ProjectApproval(SaveMixin, RESTObject):\n    _id_attr = None\n\n\nclass ProjectApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTManager):\n    _path = \"/projects/{project_id}/approvals\"\n    _obj_cls = ProjectApproval\n    _from_parent_attrs = {\"project_id\": \"id\"}\n    _update_attrs = RequiredOptional(\n        optional=(\n            \"approvals_before_merge\",\n            \"reset_approvals_on_push\",\n            \"disable_overriding_approvers_per_merge_request\",\n            \"merge_requests_author_approval\",\n            \"merge_requests_disable_committers_approval\",\n        ),\n    )\n    _update_method = UpdateMethod.POST\n\n    def get(self, **kwargs: Any) -> ProjectApproval:\n        return cast(ProjectApproval, super().get(**kwargs))\n\n\nclass ProjectApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject):\n    _id_attr = \"id\"\n\n\nclass ProjectApprovalRuleManager(\n    ListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager\n):\n    _path = \"/projects/{project_id}/approval_rules\"\n    _obj_cls = ProjectApprovalRule\n    _from_parent_attrs = {\"project_id\": \"id\"}\n    _create_attrs = RequiredOptional(\n        required=(\"name\", \"approvals_required\"),\n        optional=(\"user_ids\", \"group_ids\", \"protected_branch_ids\", \"usernames\"),\n    )\n\n\nclass ProjectMergeRequestApproval(SaveMixin, RESTObject):\n    _id_attr = None\n\n\nclass ProjectMergeRequestApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTManager):\n    _path = \"/projects/{project_id}/merge_requests/{mr_iid}/approvals\"\n    _obj_cls = ProjectMergeRequestApproval\n    _from_parent_attrs = {\"project_id\": \"project_id\", \"mr_iid\": \"iid\"}\n    _update_attrs = RequiredOptional(required=(\"approvals_required\",))\n    _update_method = UpdateMethod.POST\n\n    def get(self, **kwargs: Any) -> ProjectMergeRequestApproval:\n        return cast(ProjectMergeRequestApproval, super().get(**kwargs))\n\n    @exc.on_http_error(exc.GitlabUpdateError)\n    def set_approvers(\n        self,\n        approvals_required: int,\n        approver_ids: Optional[List[int]] = None,\n        approver_group_ids: Optional[List[int]] = None,\n        approval_rule_name: str = \"name\",\n        **kwargs: Any,\n    ) -> RESTObject:\n        \"\"\"Change MR-level allowed approvers and approver groups.\n\n        Args:\n            approvals_required: The number of required approvals for this rule\n            approver_ids: User IDs that can approve MRs\n            approver_group_ids: Group IDs whose members can approve MRs\n\n        Raises:\n            GitlabAuthenticationError: If authentication is not correct\n            GitlabUpdateError: If the server failed to perform the request\n        \"\"\"\n        approver_ids = approver_ids or []\n        approver_group_ids = approver_group_ids or []\n\n        data = {\n            \"name\": approval_rule_name,\n            \"approvals_required\": approvals_required,\n            \"rule_type\": \"regular\",\n            \"user_ids\": approver_ids,\n            \"group_ids\": approver_group_ids,\n        }\n        if TYPE_CHECKING:\n            assert self._parent is not None\n        approval_rules: ProjectMergeRequestApprovalRuleManager = (\n            self._parent.approval_rules\n        )\n        # update any existing approval rule matching the name\n        existing_approval_rules = approval_rules.list()\n        for ar in existing_approval_rules:\n            if ar.name == approval_rule_name:\n                ar.user_ids = data[\"user_ids\"]\n                ar.approvals_required = data[\"approvals_required\"]\n                ar.group_ids = data[\"group_ids\"]\n                ar.save()\n                return ar\n        # if there was no rule matching the rule name, create a new one\n        return approval_rules.create(data=data, **kwargs)\n\n\nclass ProjectMergeRequestApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject):\n    _repr_attr = \"name\"\n    id: int\n    approval_rule_id: int\n    merge_request_iid: int\n\n    @exc.on_http_error(exc.GitlabUpdateError)\n    def save(self, **kwargs: Any) -> None:\n        \"\"\"Save the changes made to the object to the server.\n\n        The object is updated to match what the server returns.\n\n        Args:\n            **kwargs: Extra options to send to the server (e.g. sudo)\n\n        Raise:\n            GitlabAuthenticationError: If authentication is not correct\n            GitlabUpdateError: If the server cannot perform the request\n        \"\"\"\n        # There is a mismatch between the name of our id attribute and the put\n        # REST API name for the project_id, so we override it here.\n        self.approval_rule_id = self.id\n        self.merge_request_iid = self._parent_attrs[\"mr_iid\"]\n        self.id = self._parent_attrs[\"project_id\"]\n        # save will update self.id with the result from the server, so no need\n        # to overwrite with what it was before we overwrote it.\n        SaveMixin.save(self, **kwargs)\n\n\nclass ProjectMergeRequestApprovalRuleManager(CRUDMixin, RESTManager):\n    _path = \"/projects/{project_id}/merge_requests/{mr_iid}/approval_rules\"\n    _obj_cls = ProjectMergeRequestApprovalRule\n    _from_parent_attrs = {\"project_id\": \"project_id\", \"mr_iid\": \"iid\"}\n    _update_attrs = RequiredOptional(\n        required=(\n            \"id\",\n            \"merge_request_iid\",\n            \"approval_rule_id\",\n            \"name\",\n            \"approvals_required\",\n        ),\n        optional=(\"user_ids\", \"group_ids\"),\n    )\n    # Important: When approval_project_rule_id is set, the name, users and\n    # groups of project-level rule will be copied. The approvals_required\n    # specified will be used.\n    _create_attrs = RequiredOptional(\n        required=(\"id\", \"merge_request_iid\", \"name\", \"approvals_required\"),\n        optional=(\"approval_project_rule_id\", \"user_ids\", \"group_ids\"),\n    )\n\n    def get(\n        self, id: Union[str, int], lazy: bool = False, **kwargs: Any\n    ) -> ProjectMergeRequestApprovalRule:\n        return cast(\n            ProjectMergeRequestApprovalRule, super().get(id=id, lazy=lazy, **kwargs)\n        )\n\n    def create(\n        self, data: Optional[Dict[str, Any]] = None, **kwargs: Any\n    ) -> RESTObject:\n        \"\"\"Create a new object.\n\n        Args:\n            data: Parameters to send to the server to create the\n                         resource\n            **kwargs: Extra options to send to the server (e.g. sudo or\n                      'ref_name', 'stage', 'name', 'all')\n\n        Raises:\n            GitlabAuthenticationError: If authentication is not correct\n            GitlabCreateError: If the server cannot perform the request\n\n        Returns:\n            A new instance of the manage object class build with\n                the data sent by the server\n        \"\"\"\n        if TYPE_CHECKING:\n            assert data is not None\n        new_data = data.copy()\n        new_data[\"id\"] = self._from_parent_attrs[\"project_id\"]\n        new_data[\"merge_request_iid\"] = self._from_parent_attrs[\"mr_iid\"]\n        return CreateMixin.create(self, new_data, **kwargs)\n\n\nclass ProjectMergeRequestApprovalState(RESTObject):\n    pass\n\n\nclass ProjectMergeRequestApprovalStateManager(GetWithoutIdMixin, RESTManager):\n    _path = \"/projects/{project_id}/merge_requests/{mr_iid}/approval_state\"\n    _obj_cls = ProjectMergeRequestApprovalState\n    _from_parent_attrs = {\"project_id\": \"project_id\", \"mr_iid\": \"iid\"}\n\n    def get(self, **kwargs: Any) -> ProjectMergeRequestApprovalState:\n        return cast(ProjectMergeRequestApprovalState, super().get(**kwargs))\n", "path": "gitlab/v4/objects/merge_request_approvals.py"}], "after_files": [{"content": "from typing import Any, cast, List, Optional, TYPE_CHECKING, Union\n\nfrom gitlab import exceptions as exc\nfrom gitlab.base import RESTManager, RESTObject\nfrom gitlab.mixins import (\n    CreateMixin,\n    CRUDMixin,\n    DeleteMixin,\n    GetWithoutIdMixin,\n    ListMixin,\n    ObjectDeleteMixin,\n    SaveMixin,\n    UpdateMethod,\n    UpdateMixin,\n)\nfrom gitlab.types import RequiredOptional\n\n__all__ = [\n    \"ProjectApproval\",\n    \"ProjectApprovalManager\",\n    \"ProjectApprovalRule\",\n    \"ProjectApprovalRuleManager\",\n    \"ProjectMergeRequestApproval\",\n    \"ProjectMergeRequestApprovalManager\",\n    \"ProjectMergeRequestApprovalRule\",\n    \"ProjectMergeRequestApprovalRuleManager\",\n    \"ProjectMergeRequestApprovalState\",\n    \"ProjectMergeRequestApprovalStateManager\",\n]\n\n\nclass ProjectApproval(SaveMixin, RESTObject):\n    _id_attr = None\n\n\nclass ProjectApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTManager):\n    _path = \"/projects/{project_id}/approvals\"\n    _obj_cls = ProjectApproval\n    _from_parent_attrs = {\"project_id\": \"id\"}\n    _update_attrs = RequiredOptional(\n        optional=(\n            \"approvals_before_merge\",\n            \"reset_approvals_on_push\",\n            \"disable_overriding_approvers_per_merge_request\",\n            \"merge_requests_author_approval\",\n            \"merge_requests_disable_committers_approval\",\n        ),\n    )\n    _update_method = UpdateMethod.POST\n\n    def get(self, **kwargs: Any) -> ProjectApproval:\n        return cast(ProjectApproval, super().get(**kwargs))\n\n\nclass ProjectApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject):\n    _id_attr = \"id\"\n\n\nclass ProjectApprovalRuleManager(\n    ListMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager\n):\n    _path = \"/projects/{project_id}/approval_rules\"\n    _obj_cls = ProjectApprovalRule\n    _from_parent_attrs = {\"project_id\": \"id\"}\n    _create_attrs = RequiredOptional(\n        required=(\"name\", \"approvals_required\"),\n        optional=(\"user_ids\", \"group_ids\", \"protected_branch_ids\", \"usernames\"),\n    )\n\n\nclass ProjectMergeRequestApproval(SaveMixin, RESTObject):\n    _id_attr = None\n\n\nclass ProjectMergeRequestApprovalManager(GetWithoutIdMixin, UpdateMixin, RESTManager):\n    _path = \"/projects/{project_id}/merge_requests/{mr_iid}/approvals\"\n    _obj_cls = ProjectMergeRequestApproval\n    _from_parent_attrs = {\"project_id\": \"project_id\", \"mr_iid\": \"iid\"}\n    _update_attrs = RequiredOptional(required=(\"approvals_required\",))\n    _update_method = UpdateMethod.POST\n\n    def get(self, **kwargs: Any) -> ProjectMergeRequestApproval:\n        return cast(ProjectMergeRequestApproval, super().get(**kwargs))\n\n    @exc.on_http_error(exc.GitlabUpdateError)\n    def set_approvers(\n        self,\n        approvals_required: int,\n        approver_ids: Optional[List[int]] = None,\n        approver_group_ids: Optional[List[int]] = None,\n        approval_rule_name: str = \"name\",\n        **kwargs: Any,\n    ) -> RESTObject:\n        \"\"\"Change MR-level allowed approvers and approver groups.\n\n        Args:\n            approvals_required: The number of required approvals for this rule\n            approver_ids: User IDs that can approve MRs\n            approver_group_ids: Group IDs whose members can approve MRs\n\n        Raises:\n            GitlabAuthenticationError: If authentication is not correct\n            GitlabUpdateError: If the server failed to perform the request\n        \"\"\"\n        approver_ids = approver_ids or []\n        approver_group_ids = approver_group_ids or []\n\n        data = {\n            \"name\": approval_rule_name,\n            \"approvals_required\": approvals_required,\n            \"rule_type\": \"regular\",\n            \"user_ids\": approver_ids,\n            \"group_ids\": approver_group_ids,\n        }\n        if TYPE_CHECKING:\n            assert self._parent is not None\n        approval_rules: ProjectMergeRequestApprovalRuleManager = (\n            self._parent.approval_rules\n        )\n        # update any existing approval rule matching the name\n        existing_approval_rules = approval_rules.list()\n        for ar in existing_approval_rules:\n            if ar.name == approval_rule_name:\n                ar.user_ids = data[\"user_ids\"]\n                ar.approvals_required = data[\"approvals_required\"]\n                ar.group_ids = data[\"group_ids\"]\n                ar.save()\n                return ar\n        # if there was no rule matching the rule name, create a new one\n        return approval_rules.create(data=data, **kwargs)\n\n\nclass ProjectMergeRequestApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject):\n    _repr_attr = \"name\"\n\n\nclass ProjectMergeRequestApprovalRuleManager(CRUDMixin, RESTManager):\n    _path = \"/projects/{project_id}/merge_requests/{merge_request_iid}/approval_rules\"\n    _obj_cls = ProjectMergeRequestApprovalRule\n    _from_parent_attrs = {\"project_id\": \"project_id\", \"merge_request_iid\": \"iid\"}\n    _update_attrs = RequiredOptional(\n        required=(\n            \"id\",\n            \"merge_request_iid\",\n            \"name\",\n            \"approvals_required\",\n        ),\n        optional=(\"user_ids\", \"group_ids\"),\n    )\n    # Important: When approval_project_rule_id is set, the name, users and\n    # groups of project-level rule will be copied. The approvals_required\n    # specified will be used.\n    _create_attrs = RequiredOptional(\n        required=(\"name\", \"approvals_required\"),\n        optional=(\"approval_project_rule_id\", \"user_ids\", \"group_ids\"),\n    )\n\n    def get(\n        self, id: Union[str, int], lazy: bool = False, **kwargs: Any\n    ) -> ProjectMergeRequestApprovalRule:\n        return cast(\n            ProjectMergeRequestApprovalRule, super().get(id=id, lazy=lazy, **kwargs)\n        )\n\n\nclass ProjectMergeRequestApprovalState(RESTObject):\n    pass\n\n\nclass ProjectMergeRequestApprovalStateManager(GetWithoutIdMixin, RESTManager):\n    _path = \"/projects/{project_id}/merge_requests/{mr_iid}/approval_state\"\n    _obj_cls = ProjectMergeRequestApprovalState\n    _from_parent_attrs = {\"project_id\": \"project_id\", \"mr_iid\": \"iid\"}\n\n    def get(self, **kwargs: Any) -> ProjectMergeRequestApprovalState:\n        return cast(ProjectMergeRequestApprovalState, super().get(**kwargs))\n", "path": "gitlab/v4/objects/merge_request_approvals.py"}]}