summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNejc Habjan <hab.nejc@gmail.com>2021-12-11 13:18:49 +0100
committerJohn Villalovos <john@sodarock.com>2021-12-11 10:25:08 -0800
commite7559bfa2ee265d7d664d7a18770b0a3e80cf999 (patch)
tree70b0193d60a06d2d1cd364c2425c87c4029dc72d
parentac5defa0c09822cf2208e66218a37d3ce00ff35b (diff)
downloadgitlab-e7559bfa2ee265d7d664d7a18770b0a3e80cf999.tar.gz
feat(api): add support for Topics API
-rw-r--r--docs/api-objects.rst1
-rw-r--r--docs/gl_objects/topics.rst48
-rw-r--r--gitlab/client.py2
-rw-r--r--gitlab/v4/objects/__init__.py1
-rw-r--r--gitlab/v4/objects/topics.py27
-rw-r--r--tests/functional/api/test_topics.py21
-rw-r--r--tests/functional/conftest.py2
-rw-r--r--tests/functional/fixtures/.env2
-rw-r--r--tests/unit/objects/test_topics.py119
9 files changed, 222 insertions, 1 deletions
diff --git a/docs/api-objects.rst b/docs/api-objects.rst
index 9c089fe..984fd4f 100644
--- a/docs/api-objects.rst
+++ b/docs/api-objects.rst
@@ -53,6 +53,7 @@ API examples
gl_objects/system_hooks
gl_objects/templates
gl_objects/todos
+ gl_objects/topics
gl_objects/users
gl_objects/variables
gl_objects/sidekiq
diff --git a/docs/gl_objects/topics.rst b/docs/gl_objects/topics.rst
new file mode 100644
index 0000000..0ca46d7
--- /dev/null
+++ b/docs/gl_objects/topics.rst
@@ -0,0 +1,48 @@
+########
+Topics
+########
+
+Topics can be used to categorize projects and find similar new projects.
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.Topic`
+ + :class:`gitlab.v4.objects.TopicManager`
+ + :attr:`gitlab.Gitlab.topics`
+
+* GitLab API: https://docs.gitlab.com/ce/api/topics.html
+
+This endpoint requires admin access for creating, updating and deleting objects.
+
+Examples
+--------
+
+List project topics on the GitLab instance::
+
+ topics = gl.topics.list()
+
+Get a specific topic by its ID::
+
+ topic = gl.topics.get(topic_id)
+
+Create a new topic::
+
+ topic = gl.topics.create({"name": "my-topic"})
+
+Update a topic::
+
+ topic.description = "My new topic"
+ topic.save()
+
+ # or
+ gl.topics.update(topic_id, {"description": "My new topic"})
+
+Delete a topic::
+
+ topic.delete()
+
+ # or
+ gl.topics.delete(topic_id)
diff --git a/gitlab/client.py b/gitlab/client.py
index 0dd4a6d..d3fdaab 100644
--- a/gitlab/client.py
+++ b/gitlab/client.py
@@ -180,6 +180,8 @@ class Gitlab(object):
"""See :class:`~gitlab.v4.objects.VariableManager`"""
self.personal_access_tokens = objects.PersonalAccessTokenManager(self)
"""See :class:`~gitlab.v4.objects.PersonalAccessTokenManager`"""
+ self.topics = objects.TopicManager(self)
+ """See :class:`~gitlab.v4.objects.TopicManager`"""
def __enter__(self) -> "Gitlab":
return self
diff --git a/gitlab/v4/objects/__init__.py b/gitlab/v4/objects/__init__.py
index b1d6484..0ab3bd4 100644
--- a/gitlab/v4/objects/__init__.py
+++ b/gitlab/v4/objects/__init__.py
@@ -70,6 +70,7 @@ from .statistics import *
from .tags import *
from .templates import *
from .todos import *
+from .topics import *
from .triggers import *
from .users import *
from .variables import *
diff --git a/gitlab/v4/objects/topics.py b/gitlab/v4/objects/topics.py
new file mode 100644
index 0000000..76208ed
--- /dev/null
+++ b/gitlab/v4/objects/topics.py
@@ -0,0 +1,27 @@
+from typing import Any, cast, Union
+
+from gitlab import types
+from gitlab.base import RequiredOptional, RESTManager, RESTObject
+from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
+
+__all__ = [
+ "Topic",
+ "TopicManager",
+]
+
+
+class Topic(SaveMixin, ObjectDeleteMixin, RESTObject):
+ pass
+
+
+class TopicManager(CRUDMixin, RESTManager):
+ _path = "/topics"
+ _obj_cls = Topic
+ _create_attrs = RequiredOptional(
+ required=("name",), optional=("avatar", "description")
+ )
+ _update_attrs = RequiredOptional(optional=("avatar", "description", "name"))
+ _types = {"avatar": types.ImageAttribute}
+
+ def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Topic:
+ return cast(Topic, super().get(id=id, lazy=lazy, **kwargs))
diff --git a/tests/functional/api/test_topics.py b/tests/functional/api/test_topics.py
new file mode 100644
index 0000000..7ad71a5
--- /dev/null
+++ b/tests/functional/api/test_topics.py
@@ -0,0 +1,21 @@
+"""
+GitLab API:
+https://docs.gitlab.com/ce/api/topics.html
+"""
+
+
+def test_topics(gl):
+ assert not gl.topics.list()
+
+ topic = gl.topics.create({"name": "my-topic", "description": "My Topic"})
+ assert topic.name == "my-topic"
+ assert gl.topics.list()
+
+ topic.description = "My Updated Topic"
+ topic.save()
+
+ updated_topic = gl.topics.get(topic.id)
+ assert updated_topic.description == topic.description
+
+ topic.delete()
+ assert not gl.topics.list()
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
index 625cff9..109ee24 100644
--- a/tests/functional/conftest.py
+++ b/tests/functional/conftest.py
@@ -24,6 +24,8 @@ def reset_gitlab(gl):
for deploy_token in group.deploytokens.list():
deploy_token.delete()
group.delete()
+ for topic in gl.topics.list():
+ topic.delete()
for variable in gl.variables.list():
variable.delete()
for user in gl.users.list():
diff --git a/tests/functional/fixtures/.env b/tests/functional/fixtures/.env
index 374f7ac..30abd5c 100644
--- a/tests/functional/fixtures/.env
+++ b/tests/functional/fixtures/.env
@@ -1,2 +1,2 @@
GITLAB_IMAGE=gitlab/gitlab-ce
-GITLAB_TAG=14.3.2-ce.0
+GITLAB_TAG=14.5.2-ce.0
diff --git a/tests/unit/objects/test_topics.py b/tests/unit/objects/test_topics.py
new file mode 100644
index 0000000..14b2cfd
--- /dev/null
+++ b/tests/unit/objects/test_topics.py
@@ -0,0 +1,119 @@
+"""
+GitLab API:
+https://docs.gitlab.com/ce/api/topics.html
+"""
+import pytest
+import responses
+
+from gitlab.v4.objects import Topic
+
+name = "GitLab"
+new_name = "gitlab-test"
+topic_content = {
+ "id": 1,
+ "name": name,
+ "description": "GitLab is an open source end-to-end software development platform.",
+ "total_projects_count": 1000,
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+}
+topics_url = "http://localhost/api/v4/topics"
+topic_url = f"{topics_url}/1"
+
+
+@pytest.fixture
+def resp_list_topics():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=topics_url,
+ json=[topic_content],
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_get_topic():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.GET,
+ url=topic_url,
+ json=topic_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_create_topic():
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.POST,
+ url=topics_url,
+ json=topic_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_update_topic():
+ updated_content = dict(topic_content)
+ updated_content["name"] = new_name
+
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.PUT,
+ url=topic_url,
+ json=updated_content,
+ content_type="application/json",
+ status=200,
+ )
+ yield rsps
+
+
+@pytest.fixture
+def resp_delete_topic(no_content):
+ with responses.RequestsMock() as rsps:
+ rsps.add(
+ method=responses.DELETE,
+ url=topic_url,
+ json=no_content,
+ content_type="application/json",
+ status=204,
+ )
+ yield rsps
+
+
+def test_list_topics(gl, resp_list_topics):
+ topics = gl.topics.list()
+ assert isinstance(topics, list)
+ assert isinstance(topics[0], Topic)
+ assert topics[0].name == name
+
+
+def test_get_topic(gl, resp_get_topic):
+ topic = gl.topics.get(1)
+ assert isinstance(topic, Topic)
+ assert topic.name == name
+
+
+def test_create_topic(gl, resp_create_topic):
+ topic = gl.topics.create({"name": name})
+ assert isinstance(topic, Topic)
+ assert topic.name == name
+
+
+def test_update_topic(gl, resp_update_topic):
+ topic = gl.topics.get(1, lazy=True)
+ topic.name = new_name
+ topic.save()
+ assert topic.name == new_name
+
+
+def test_delete_topic(gl, resp_delete_topic):
+ topic = gl.topics.get(1, lazy=True)
+ topic.delete()