Repository: python-gitlab/python-gitlab

Issue:
Add application statistics
## Description of the problem, including code/CLI snippet

The Gitlab provides some general statistics via `/application/statistics`.
See https://docs.gitlab.com/ee/api/statistics.html

## Expected Behavior

These data are available via `gitlab.statistics()`.

## Actual Behavior

Nothing.

## Specifications

  - python-gitlab version:
  - API version you are using (v3/v4):
  - Gitlab server version (or gitlab.com):



Discussion:
@JohnVillalovos is this issue up for grabs? I see the `help wanted` label but I'm not sure if we're missing any details?
> @JohnVillalovos is this issue up for grabs? I see the `help wanted` label but I'm not sure if we're missing any details?

Feel free to work on it. If you want to work on it and are able to then please assign it to yourself. If you can't assign it to yourself but want to work on it than reply here and we can assign it to you.
@JohnVillalovos I assigned @Shreya-7  as I think only we can (https://docs.github.com/en/issues/tracking-your-work-with-issues/assigning-issues-and-pull-requests-to-other-github-users)

You can take a look at https://github.com/python-gitlab/python-gitlab/blob/main/gitlab/v4/objects/appearance.py for something similar :)



Resolving patch:
diff --git a/gitlab/client.py b/gitlab/client.py
--- a/gitlab/client.py
+++ b/gitlab/client.py
@@ -188,6 +188,8 @@ def __init__(
         """See :class:`~gitlab.v4.objects.PersonalAccessTokenManager`"""
         self.topics = objects.TopicManager(self)
         """See :class:`~gitlab.v4.objects.TopicManager`"""
+        self.statistics = objects.ApplicationStatisticsManager(self)
+        """See :class:`~gitlab.v4.objects.ApplicationStatisticsManager`"""
 
     def __enter__(self) -> "Gitlab":
         return self
diff --git a/gitlab/v4/objects/statistics.py b/gitlab/v4/objects/statistics.py
--- a/gitlab/v4/objects/statistics.py
+++ b/gitlab/v4/objects/statistics.py
@@ -13,6 +13,8 @@
     "IssuesStatisticsManager",
     "ProjectIssuesStatistics",
     "ProjectIssuesStatisticsManager",
+    "ApplicationStatistics",
+    "ApplicationStatisticsManager",
 ]
 
 
@@ -71,3 +73,15 @@ class ProjectIssuesStatisticsManager(GetWithoutIdMixin, RESTManager):
 
     def get(self, **kwargs: Any) -> ProjectIssuesStatistics:
         return cast(ProjectIssuesStatistics, super().get(**kwargs))
+
+
+class ApplicationStatistics(RESTObject):
+    _id_attr = None
+
+
+class ApplicationStatisticsManager(GetWithoutIdMixin, RESTManager):
+    _path = "/application/statistics"
+    _obj_cls = ApplicationStatistics
+
+    def get(self, **kwargs: Any) -> ApplicationStatistics:
+        return cast(ApplicationStatistics, super().get(**kwargs))


Test patch:
diff --git a/tests/functional/api/test_statistics.py b/tests/functional/api/test_statistics.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/api/test_statistics.py
@@ -0,0 +1,12 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/statistics.html
+"""
+
+
+def test_get_statistics(gl):
+    statistics = gl.statistics.get()
+
+    assert statistics.snippets.isdigit()
+    assert statistics.users.isdigit()
+    assert statistics.groups.isdigit()
+    assert statistics.projects.isdigit()
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
--- a/tests/functional/conftest.py
+++ b/tests/functional/conftest.py
@@ -94,7 +94,18 @@ def reset_gitlab(gl: gitlab.Gitlab) -> None:
             helpers.safe_delete(deploy_token)
         logging.info(f"Deleting project: {project.path_with_namespace!r}")
         helpers.safe_delete(project)
+
     for group in gl.groups.list():
+
+        # skip deletion of a descendant group to prevent scenarios where parent group
+        # gets deleted leaving a dangling descendant whose deletion will throw 404s.
+        if group.parent_id:
+            logging.info(
+                f"Skipping deletion of {group.full_path} as it is a descendant "
+                f"group and will be removed when the parent group is deleted"
+            )
+            continue
+
         for deploy_token in group.deploytokens.list():
             logging.info(
                 f"Deleting deploy token: {deploy_token.username!r} in "
@@ -110,7 +121,7 @@ def reset_gitlab(gl: gitlab.Gitlab) -> None:
         logging.info(f"Deleting variable: {variable.key!r}")
         helpers.safe_delete(variable)
     for user in gl.users.list():
-        if user.username != "root":
+        if user.username not in ["root", "ghost"]:
             logging.info(f"Deleting user: {user.username!r}")
             helpers.safe_delete(user, hard_delete=True)
 
diff --git a/tests/unit/objects/test_statistics.py b/tests/unit/objects/test_statistics.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/objects/test_statistics.py
@@ -0,0 +1,49 @@
+"""
+GitLab API: https://docs.gitlab.com/ee/api/statistics.html
+"""
+
+import pytest
+import responses
+
+content = {
+    "forks": "10",
+    "issues": "76",
+    "merge_requests": "27",
+    "notes": "954",
+    "snippets": "50",
+    "ssh_keys": "10",
+    "milestones": "40",
+    "users": "50",
+    "groups": "10",
+    "projects": "20",
+    "active_users": "50",
+}
+
+
+@pytest.fixture
+def resp_application_statistics():
+
+    with responses.RequestsMock() as rsps:
+        rsps.add(
+            method=responses.GET,
+            url="http://localhost/api/v4/application/statistics",
+            json=content,
+            content_type="application/json",
+            status=200,
+        )
+
+        yield rsps
+
+
+def test_get_statistics(gl, resp_application_statistics):
+    statistics = gl.statistics.get()
+    assert statistics.forks == content["forks"]
+    assert statistics.merge_requests == content["merge_requests"]
+    assert statistics.notes == content["notes"]
+    assert statistics.snippets == content["snippets"]
+    assert statistics.ssh_keys == content["ssh_keys"]
+    assert statistics.milestones == content["milestones"]
+    assert statistics.users == content["users"]
+    assert statistics.groups == content["groups"]
+    assert statistics.projects == content["projects"]
+    assert statistics.active_users == content["active_users"]