Repository: python-gitlab/python-gitlab

Issue:
Add support for resource groups
See https://docs.gitlab.com/ee/api/resource_groups.html


Discussion:

Resolving patch:
diff --git a/gitlab/v4/objects/__init__.py b/gitlab/v4/objects/__init__.py
--- a/gitlab/v4/objects/__init__.py
+++ b/gitlab/v4/objects/__init__.py
@@ -52,6 +52,7 @@
 from .push_rules import *
 from .releases import *
 from .repositories import *
+from .resource_groups import *
 from .runners import *
 from .secure_files import *
 from .settings import *
diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py
--- a/gitlab/v4/objects/projects.py
+++ b/gitlab/v4/objects/projects.py
@@ -81,6 +81,7 @@
 from .push_rules import ProjectPushRulesManager  # noqa: F401
 from .releases import ProjectReleaseManager  # noqa: F401
 from .repositories import RepositoryMixin
+from .resource_groups import ProjectResourceGroupManager
 from .runners import ProjectRunnerManager  # noqa: F401
 from .secure_files import ProjectSecureFileManager  # noqa: F401
 from .snippets import ProjectSnippetManager  # noqa: F401
@@ -207,6 +208,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
     protectedtags: ProjectProtectedTagManager
     pushrules: ProjectPushRulesManager
     releases: ProjectReleaseManager
+    resource_groups: ProjectResourceGroupManager
     remote_mirrors: "ProjectRemoteMirrorManager"
     repositories: ProjectRegistryRepositoryManager
     runners: ProjectRunnerManager
diff --git a/gitlab/v4/objects/resource_groups.py b/gitlab/v4/objects/resource_groups.py
new file mode 100644
--- /dev/null
+++ b/gitlab/v4/objects/resource_groups.py
@@ -0,0 +1,45 @@
+from typing import Any, cast, Union
+
+from gitlab.base import RESTManager, RESTObject
+from gitlab.mixins import ListMixin, RetrieveMixin, SaveMixin, UpdateMixin
+from gitlab.types import RequiredOptional
+
+__all__ = [
+    "ProjectResourceGroup",
+    "ProjectResourceGroupManager",
+    "ProjectResourceGroupUpcomingJob",
+    "ProjectResourceGroupUpcomingJobManager",
+]
+
+
+class ProjectResourceGroup(SaveMixin, RESTObject):
+    _id_attr = "key"
+
+    upcoming_jobs: "ProjectResourceGroupUpcomingJobManager"
+
+
+class ProjectResourceGroupManager(RetrieveMixin, UpdateMixin, RESTManager):
+    _path = "/projects/{project_id}/resource_groups"
+    _obj_cls = ProjectResourceGroup
+    _from_parent_attrs = {"project_id": "id"}
+    _list_filters = (
+        "order_by",
+        "sort",
+        "include_html_description",
+    )
+    _update_attrs = RequiredOptional(optional=("process_mode",))
+
+    def get(
+        self, id: Union[str, int], lazy: bool = False, **kwargs: Any
+    ) -> ProjectResourceGroup:
+        return cast(ProjectResourceGroup, super().get(id=id, lazy=lazy, **kwargs))
+
+
+class ProjectResourceGroupUpcomingJob(RESTObject):
+    pass
+
+
+class ProjectResourceGroupUpcomingJobManager(ListMixin, RESTManager):
+    _path = "/projects/{project_id}/resource_groups/{resource_group_key}/upcoming_jobs"
+    _obj_cls = ProjectResourceGroupUpcomingJob
+    _from_parent_attrs = {"project_id": "project_id", "resource_group_key": "key"}


Test patch:
diff --git a/tests/unit/objects/test_resource_groups.py b/tests/unit/objects/test_resource_groups.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/objects/test_resource_groups.py
@@ -0,0 +1,79 @@
+"""
+GitLab API:
+https://docs.gitlab.com/ee/api/resource_groups.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import ProjectResourceGroup, ProjectResourceGroupUpcomingJob
+
+from .test_jobs import job_content
+
+resource_group_content = {
+    "id": 3,
+    "key": "production",
+    "process_mode": "unordered",
+    "created_at": "2021-09-01T08:04:59.650Z",
+    "updated_at": "2021-09-01T08:04:59.650Z",
+}
+
+
+@pytest.fixture
+def resp_list_resource_groups():
+    with responses.RequestsMock() as rsps:
+        rsps.add(
+            method=responses.GET,
+            url="http://localhost/api/v4/projects/1/resource_groups",
+            json=[resource_group_content],
+            content_type="application/json",
+            status=200,
+        )
+        yield rsps
+
+
+@pytest.fixture
+def resp_get_resource_group():
+    with responses.RequestsMock() as rsps:
+        rsps.add(
+            method=responses.GET,
+            url="http://localhost/api/v4/projects/1/resource_groups/production",
+            json=resource_group_content,
+            content_type="application/json",
+            status=200,
+        )
+        yield rsps
+
+
+@pytest.fixture
+def resp_list_upcoming_jobs():
+    with responses.RequestsMock() as rsps:
+        rsps.add(
+            method=responses.GET,
+            url="http://localhost/api/v4/projects/1/resource_groups/production/upcoming_jobs",
+            json=[job_content],
+            content_type="application/json",
+            status=200,
+        )
+        yield rsps
+
+
+def test_list_project_resource_groups(project, resp_list_resource_groups):
+    resource_groups = project.resource_groups.list()
+    assert isinstance(resource_groups, list)
+    assert isinstance(resource_groups[0], ProjectResourceGroup)
+    assert resource_groups[0].process_mode == "unordered"
+
+
+def test_get_project_resource_group(project, resp_get_resource_group):
+    resource_group = project.resource_groups.get("production")
+    assert isinstance(resource_group, ProjectResourceGroup)
+    assert resource_group.process_mode == "unordered"
+
+
+def test_list_resource_group_upcoming_jobs(project, resp_list_upcoming_jobs):
+    resource_group = project.resource_groups.get("production", lazy=True)
+    upcoming_jobs = resource_group.upcoming_jobs.list()
+
+    assert isinstance(upcoming_jobs, list)
+    assert isinstance(upcoming_jobs[0], ProjectResourceGroupUpcomingJob)
+    assert upcoming_jobs[0].ref == "main"