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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 48 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 27 additions & 0 deletions
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

Lines changed: 21 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 119 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy