Repository: python-gitlab/python-gitlab

Issue:
List shared projects of a group
> The projects and shared_projects attributes in the response are deprecated and [scheduled for removal in API v5](https://gitlab.com/gitlab-org/gitlab/-/issues/213797). To get the details of all projects within a group, use either the [list a group’s projects](https://docs.gitlab.com/ee/api/groups.html#list-a-groups-projects) or the [list a group’s shared projects](https://docs.gitlab.com/ee/api/groups.html#list-a-groups-shared-projects) endpoint.

https://docs.gitlab.com/ee/api/groups.html#list-a-groups-shared-projects


Discussion:

Resolving patch:
diff --git a/gitlab/v4/objects/groups.py b/gitlab/v4/objects/groups.py
--- a/gitlab/v4/objects/groups.py
+++ b/gitlab/v4/objects/groups.py
@@ -34,7 +34,7 @@
 from .milestones import GroupMilestoneManager  # noqa: F401
 from .notification_settings import GroupNotificationSettingsManager  # noqa: F401
 from .packages import GroupPackageManager  # noqa: F401
-from .projects import GroupProjectManager  # noqa: F401
+from .projects import GroupProjectManager, SharedProjectManager  # noqa: F401
 from .push_rules import GroupPushRulesManager
 from .runners import GroupRunnerManager  # noqa: F401
 from .statistics import GroupIssuesStatisticsManager  # noqa: F401
@@ -79,6 +79,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
     notificationsettings: GroupNotificationSettingsManager
     packages: GroupPackageManager
     projects: GroupProjectManager
+    shared_projects: SharedProjectManager
     pushrules: GroupPushRulesManager
     registry_repositories: GroupRegistryRepositoryManager
     runners: GroupRunnerManager
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
@@ -103,6 +103,8 @@
     "ProjectRemoteMirrorManager",
     "ProjectStorage",
     "ProjectStorageManager",
+    "SharedProject",
+    "SharedProjectManager",
 ]
 
 
@@ -1081,3 +1083,26 @@ class ProjectStorageManager(GetWithoutIdMixin, RESTManager):
 
     def get(self, **kwargs: Any) -> ProjectStorage:
         return cast(ProjectStorage, super().get(**kwargs))
+
+
+class SharedProject(RESTObject):
+    pass
+
+
+class SharedProjectManager(ListMixin, RESTManager):
+    _path = "/groups/{group_id}/projects/shared"
+    _obj_cls = SharedProject
+    _from_parent_attrs = {"group_id": "id"}
+    _list_filters = (
+        "archived",
+        "visibility",
+        "order_by",
+        "sort",
+        "search",
+        "simple",
+        "starred",
+        "with_issues_enabled",
+        "with_merge_requests_enabled",
+        "min_access_level",
+        "with_custom_attributes",
+    )


Test patch:
diff --git a/tests/unit/objects/test_groups.py b/tests/unit/objects/test_groups.py
--- a/tests/unit/objects/test_groups.py
+++ b/tests/unit/objects/test_groups.py
@@ -9,8 +9,21 @@
 
 import gitlab
 from gitlab.v4.objects import GroupDescendantGroup, GroupSubgroup
+from gitlab.v4.objects.projects import GroupProject, SharedProject
 
 content = {"name": "name", "id": 1, "path": "path"}
+projects_content = [
+    {
+        "id": 9,
+        "description": "foo",
+        "default_branch": "master",
+        "name": "Html5 Boilerplate",
+        "name_with_namespace": "Experimental / Html5 Boilerplate",
+        "path": "html5-boilerplate",
+        "path_with_namespace": "h5bp/html5-boilerplate",
+        "namespace": {"id": 5, "name": "Experimental", "path": "h5bp", "kind": "group"},
+    }
+]
 subgroup_descgroup_content = [
     {
         "id": 2,
@@ -80,6 +93,19 @@ def resp_groups():
         yield rsps
 
 
+@pytest.fixture
+def resp_list_group_projects():
+    with responses.RequestsMock() as rsps:
+        rsps.add(
+            method=responses.GET,
+            url=re.compile(r"http://localhost/api/v4/groups/1/projects(/shared)?"),
+            json=projects_content,
+            content_type="application/json",
+            status=200,
+        )
+        yield rsps
+
+
 @pytest.fixture
 def resp_list_subgroups_descendant_groups():
     with responses.RequestsMock() as rsps:
@@ -211,6 +237,18 @@ def test_create_group_export(group, resp_export):
     assert export.message == "202 Accepted"
 
 
+def test_list_group_projects(group, resp_list_group_projects):
+    projects = group.projects.list()
+    assert isinstance(projects[0], GroupProject)
+    assert projects[0].path == projects_content[0]["path"]
+
+
+def test_list_group_shared_projects(group, resp_list_group_projects):
+    projects = group.shared_projects.list()
+    assert isinstance(projects[0], SharedProject)
+    assert projects[0].path == projects_content[0]["path"]
+
+
 def test_list_group_subgroups(group, resp_list_subgroups_descendant_groups):
     subgroups = group.subgroups.list()
     assert isinstance(subgroups[0], GroupSubgroup)