Skip to content

Commit e7559bf

Browse files
nejchJohnVillalovos
authored andcommitted
feat(api): add support for Topics API
1 parent ac5defa commit e7559bf

File tree

9 files changed

+222
-1
lines changed

9 files changed

+222
-1
lines changed

docs/api-objects.rst

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ API examples
5353
gl_objects/system_hooks
5454
gl_objects/templates
5555
gl_objects/todos
56+
gl_objects/topics
5657
gl_objects/users
5758
gl_objects/variables
5859
gl_objects/sidekiq

docs/gl_objects/topics.rst

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
########
2+
Topics
3+
########
4+
5+
Topics can be used to categorize projects and find similar new projects.
6+
7+
Reference
8+
---------
9+
10+
* v4 API:
11+
12+
+ :class:`gitlab.v4.objects.Topic`
13+
+ :class:`gitlab.v4.objects.TopicManager`
14+
+ :attr:`gitlab.Gitlab.topics`
15+
16+
* GitLab API: https://docs.gitlab.com/ce/api/topics.html
17+
18+
This endpoint requires admin access for creating, updating and deleting objects.
19+
20+
Examples
21+
--------
22+
23+
List project topics on the GitLab instance::
24+
25+
topics = gl.topics.list()
26+
27+
Get a specific topic by its ID::
28+
29+
topic = gl.topics.get(topic_id)
30+
31+
Create a new topic::
32+
33+
topic = gl.topics.create({"name": "my-topic"})
34+
35+
Update a topic::
36+
37+
topic.description = "My new topic"
38+
topic.save()
39+
40+
# or
41+
gl.topics.update(topic_id, {"description": "My new topic"})
42+
43+
Delete a topic::
44+
45+
topic.delete()
46+
47+
# or
48+
gl.topics.delete(topic_id)

gitlab/client.py

+2
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ def __init__(
180180
"""See :class:`~gitlab.v4.objects.VariableManager`"""
181181
self.personal_access_tokens = objects.PersonalAccessTokenManager(self)
182182
"""See :class:`~gitlab.v4.objects.PersonalAccessTokenManager`"""
183+
self.topics = objects.TopicManager(self)
184+
"""See :class:`~gitlab.v4.objects.TopicManager`"""
183185

184186
def __enter__(self) -> "Gitlab":
185187
return self

gitlab/v4/objects/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
from .tags import *
7171
from .templates import *
7272
from .todos import *
73+
from .topics import *
7374
from .triggers import *
7475
from .users import *
7576
from .variables import *

gitlab/v4/objects/topics.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import Any, cast, Union
2+
3+
from gitlab import types
4+
from gitlab.base import RequiredOptional, RESTManager, RESTObject
5+
from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
6+
7+
__all__ = [
8+
"Topic",
9+
"TopicManager",
10+
]
11+
12+
13+
class Topic(SaveMixin, ObjectDeleteMixin, RESTObject):
14+
pass
15+
16+
17+
class TopicManager(CRUDMixin, RESTManager):
18+
_path = "/topics"
19+
_obj_cls = Topic
20+
_create_attrs = RequiredOptional(
21+
required=("name",), optional=("avatar", "description")
22+
)
23+
_update_attrs = RequiredOptional(optional=("avatar", "description", "name"))
24+
_types = {"avatar": types.ImageAttribute}
25+
26+
def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Topic:
27+
return cast(Topic, super().get(id=id, lazy=lazy, **kwargs))

tests/functional/api/test_topics.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
GitLab API:
3+
https://docs.gitlab.com/ce/api/topics.html
4+
"""
5+
6+
7+
def test_topics(gl):
8+
assert not gl.topics.list()
9+
10+
topic = gl.topics.create({"name": "my-topic", "description": "My Topic"})
11+
assert topic.name == "my-topic"
12+
assert gl.topics.list()
13+
14+
topic.description = "My Updated Topic"
15+
topic.save()
16+
17+
updated_topic = gl.topics.get(topic.id)
18+
assert updated_topic.description == topic.description
19+
20+
topic.delete()
21+
assert not gl.topics.list()

tests/functional/conftest.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def reset_gitlab(gl):
2424
for deploy_token in group.deploytokens.list():
2525
deploy_token.delete()
2626
group.delete()
27+
for topic in gl.topics.list():
28+
topic.delete()
2729
for variable in gl.variables.list():
2830
variable.delete()
2931
for user in gl.users.list():

tests/functional/fixtures/.env

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
GITLAB_IMAGE=gitlab/gitlab-ce
2-
GITLAB_TAG=14.3.2-ce.0
2+
GITLAB_TAG=14.5.2-ce.0

tests/unit/objects/test_topics.py

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""
2+
GitLab API:
3+
https://docs.gitlab.com/ce/api/topics.html
4+
"""
5+
import pytest
6+
import responses
7+
8+
from gitlab.v4.objects import Topic
9+
10+
name = "GitLab"
11+
new_name = "gitlab-test"
12+
topic_content = {
13+
"id": 1,
14+
"name": name,
15+
"description": "GitLab is an open source end-to-end software development platform.",
16+
"total_projects_count": 1000,
17+
"avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
18+
}
19+
topics_url = "http://localhost/api/v4/topics"
20+
topic_url = f"{topics_url}/1"
21+
22+
23+
@pytest.fixture
24+
def resp_list_topics():
25+
with responses.RequestsMock() as rsps:
26+
rsps.add(
27+
method=responses.GET,
28+
url=topics_url,
29+
json=[topic_content],
30+
content_type="application/json",
31+
status=200,
32+
)
33+
yield rsps
34+
35+
36+
@pytest.fixture
37+
def resp_get_topic():
38+
with responses.RequestsMock() as rsps:
39+
rsps.add(
40+
method=responses.GET,
41+
url=topic_url,
42+
json=topic_content,
43+
content_type="application/json",
44+
status=200,
45+
)
46+
yield rsps
47+
48+
49+
@pytest.fixture
50+
def resp_create_topic():
51+
with responses.RequestsMock() as rsps:
52+
rsps.add(
53+
method=responses.POST,
54+
url=topics_url,
55+
json=topic_content,
56+
content_type="application/json",
57+
status=200,
58+
)
59+
yield rsps
60+
61+
62+
@pytest.fixture
63+
def resp_update_topic():
64+
updated_content = dict(topic_content)
65+
updated_content["name"] = new_name
66+
67+
with responses.RequestsMock() as rsps:
68+
rsps.add(
69+
method=responses.PUT,
70+
url=topic_url,
71+
json=updated_content,
72+
content_type="application/json",
73+
status=200,
74+
)
75+
yield rsps
76+
77+
78+
@pytest.fixture
79+
def resp_delete_topic(no_content):
80+
with responses.RequestsMock() as rsps:
81+
rsps.add(
82+
method=responses.DELETE,
83+
url=topic_url,
84+
json=no_content,
85+
content_type="application/json",
86+
status=204,
87+
)
88+
yield rsps
89+
90+
91+
def test_list_topics(gl, resp_list_topics):
92+
topics = gl.topics.list()
93+
assert isinstance(topics, list)
94+
assert isinstance(topics[0], Topic)
95+
assert topics[0].name == name
96+
97+
98+
def test_get_topic(gl, resp_get_topic):
99+
topic = gl.topics.get(1)
100+
assert isinstance(topic, Topic)
101+
assert topic.name == name
102+
103+
104+
def test_create_topic(gl, resp_create_topic):
105+
topic = gl.topics.create({"name": name})
106+
assert isinstance(topic, Topic)
107+
assert topic.name == name
108+
109+
110+
def test_update_topic(gl, resp_update_topic):
111+
topic = gl.topics.get(1, lazy=True)
112+
topic.name = new_name
113+
topic.save()
114+
assert topic.name == new_name
115+
116+
117+
def test_delete_topic(gl, resp_delete_topic):
118+
topic = gl.topics.get(1, lazy=True)
119+
topic.delete()

0 commit comments

Comments
 (0)