Repository navigation
Add read-only support for the Group epic boards API (/groups/:id/epic_boards) #3460
Description
Description of the problem, including code/CLI snippet
Location (python-gitlab/python-gitlab @ default branch 8daa940):
badges: GroupBadgeManager
billable_members: GroupBillableMemberManager
boards: GroupBoardManager
clusters: GroupClusterManager
customattributes: GroupCustomAttributeManager
deploytokens: GroupDeployTokenManager
descendant_groups: GroupDescendantGroupManager
epics: GroupEpicManager
exports: GroupExportManager
hooks: GroupHookManager
imports: GroupImportManagerProblem
GitLab documents a Group epic boards API (Premium/Ultimate) with four read endpoints:
GET /groups/:id/epic_boardsGET /groups/:id/epic_boards/:board_idGET /groups/:id/epic_boards/:board_id/listsGET /groups/:id/epic_boards/:board_id/lists/:list_id
python-gitlab exposes group issue boards (group.boards, with board.lists), but there is no manager for epic boards: the Group class has boards and epics but no epic-board attribute, and boards.py only defines issue-board managers. Listing epic boards and their columns (e.g. for board inventory or migration scripts) therefore requires raw gl.http_get(...) calls.
Reproduction
group = gl.groups.get(5, lazy=True)
group.epicboards.list()
# AttributeError: 'Group' object has no attribute 'epicboards'Environment: main at 8daa94041261 (8.6.0); also absent at 51eb17e6e67a.
Expected Behavior
Typed managers analogous to the existing group board managers, e.g.:
boards = group.epicboards.list() # GET /groups/5/epic_boards
board = group.epicboards.get(1) # GET /groups/5/epic_boards/1
lists = board.lists.list() # GET /groups/5/epic_boards/1/lists
lst = board.lists.get(2) # GET /groups/5/epic_boards/1/lists/2Proposed scope: list/get for boards and board lists only, i.e. exactly what the REST page documents today. No create/update/delete (not documented for this resource), and nothing related to the deprecated Epics REST API or Work Items. The attribute name (epicboards, epic_boards, ...) is up to the maintainers.
Actual Behavior
AttributeError: 'Group' object has no attribute 'epicboards'; the only workaround is gl.http_get(f"/groups/{gid}/epic_boards") / gl.http_list(...), which returns plain dicts.
Already implemented in xanzy/go-gitlab
PR: xanzy/go-gitlab#1764 — "Epic Boards" (merged 2023-07-30T16:08:48Z, merge commit fd3ac6371e6). It adds a GroupEpicBoardsService with ListGroupEpicBoards and GetGroupEpicBoard, covering the board-level endpoints; the /lists endpoints are not part of this PR. go-gitlab is a separate client for the same GitLab REST API, so this is precedent, not an obligation.
How go-gitlab fixed it:
--- a/group_epic_boards.go
+++ b/group_epic_boards.go
@@ -0,0 +1,104 @@
...
+// GroupEpicBoard represents a GitLab group epic board.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_epic_boards.html
+type GroupEpicBoard struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Group *Group `json:"group"`
+ Labels []*LabelDetails `json:"labels"`
+ Lists []*BoardList `json:"lists"`
+}
...
+func (s *GroupEpicBoardsService) ListGroupEpicBoards(gid interface{}, opt *ListGroupEpicBoardsOptions, options ...RequestOptionFunc) ([]*GroupEpicBoard, *Response, error) {
+ group, err := parseID(gid)
+ if err != nil {
+ return nil, nil, err
+ }
+ u := fmt.Sprintf("groups/%s/epic_boards", PathEscape(group))
...
+func (s *GroupEpicBoardsService) GetGroupEpicBoard(gid interface{}, board int, options ...RequestOptionFunc) (*GroupEpicBoard, *Response, error) {
+ group, err := parseID(gid)
+ if err != nil {
+ return nil, nil, err
+ }
+ u := fmt.Sprintf("groups/%s/epic_boards/%d", PathEscape(group), board)Suggested fix
Add read-only (RetrieveMixin) group epic board and board list managers next to the existing group board managers in boards.py, attach the board manager to Group as epicboards, and document them; this covers the two board endpoints go-gitlab has plus the two documented /lists endpoints:
diff --git a/docs/gl_objects/boards.rst b/docs/gl_objects/boards.rst
--- a/docs/gl_objects/boards.rst
+++ b/docs/gl_objects/boards.rst
@@ -102,3 +102,39 @@ set it at the given position and move the following lists up a position::
Delete a list::
b_list.delete()
+
+Group epic boards
+=================
+
+Epic boards show the epics of a group, organized in lists. Only read access
+is available through the API.
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.GroupEpicBoard`
+ + :class:`gitlab.v4.objects.GroupEpicBoardManager`
+ + :attr:`gitlab.v4.objects.Group.epicboards`
+ + :class:`gitlab.v4.objects.GroupEpicBoardList`
+ + :class:`gitlab.v4.objects.GroupEpicBoardListManager`
+ + :attr:`gitlab.v4.objects.GroupEpicBoard.lists`
+
+* GitLab API: https://docs.gitlab.com/api/group_epic_boards (EE feature)
+
+Examples
+--------
+
+List the epic boards of a group::
+
+ boards = group.epicboards.list(get_all=True)
+
+Get a single epic board::
+
+ board = group.epicboards.get(board_id)
+
+List the lists of an epic board, or get a single list::
+
+ b_lists = board.lists.list(get_all=True)
+ b_list = board.lists.get(list_id)
diff --git a/gitlab/v4/objects/boards.py b/gitlab/v4/objects/boards.py
--- a/gitlab/v4/objects/boards.py
+++ b/gitlab/v4/objects/boards.py
@@ -1,5 +1,5 @@
from gitlab.base import RESTObject
-from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
+from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, RetrieveMixin, SaveMixin
from gitlab.types import RequiredOptional
__all__ = [
@@ -7,6 +7,10 @@ __all__ = [
"GroupBoardListManager",
"GroupBoard",
"GroupBoardManager",
+ "GroupEpicBoardList",
+ "GroupEpicBoardListManager",
+ "GroupEpicBoard",
+ "GroupEpicBoardManager",
"ProjectBoardList",
"ProjectBoardListManager",
"ProjectBoard",
@@ -39,6 +43,26 @@ class GroupBoardManager(CRUDMixin[GroupBoard]):
_create_attrs = RequiredOptional(required=("name",))
+class GroupEpicBoardList(RESTObject):
+ pass
+
+
+class GroupEpicBoardListManager(RetrieveMixin[GroupEpicBoardList]):
+ _path = "/groups/{group_id}/epic_boards/{board_id}/lists"
+ _obj_cls = GroupEpicBoardList
+ _from_parent_attrs = {"group_id": "group_id", "board_id": "id"}
+
+
+class GroupEpicBoard(RESTObject):
+ lists: GroupEpicBoardListManager
+
+
+class GroupEpicBoardManager(RetrieveMixin[GroupEpicBoard]):
+ _path = "/groups/{group_id}/epic_boards"
+ _obj_cls = GroupEpicBoard
+ _from_parent_attrs = {"group_id": "id"}
+
+
class ProjectBoardList(SaveMixin, ObjectDeleteMixin, RESTObject):
pass
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
@@ -23,7 +23,7 @@ from gitlab.types import RequiredOptional
from .access_requests import GroupAccessRequestManager # noqa: F401
from .audit_events import GroupAuditEventManager # noqa: F401
from .badges import GroupBadgeManager # noqa: F401
-from .boards import GroupBoardManager # noqa: F401
+from .boards import GroupBoardManager, GroupEpicBoardManager # noqa: F401
from .branches import GroupProtectedBranchManager # noqa: F401
from .clusters import GroupClusterManager # noqa: F401
from .container_registry import GroupRegistryRepositoryManager # noqa: F401
@@ -84,6 +84,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
customattributes: GroupCustomAttributeManager
deploytokens: GroupDeployTokenManager
descendant_groups: GroupDescendantGroupManager
+ epicboards: GroupEpicBoardManager
epics: GroupEpicManager
exports: GroupExportManager
hooks: GroupHookManagerVerified on 51eb17e (Python 3.14.10, pytest 9.1.1): a responses-based unit test for the four endpoints (tests/unit/objects/test_group_epic_boards.py, which would go into the PR) fails without this patch with the AttributeError above and passes with it; no other test results change.
I can submit a PR for this.
Checked: searches for epic board, epic_boards, epicboards, "epic boards" in this repository found no existing issue or PR.
Specifications
- python-gitlab version:
mainat8daa94041261(8.6.0); also absent at51eb17e6e67a - Gitlab server version (or gitlab.com): API documented for GitLab Premium/Ultimate (GitLab.com, Self-Managed, Dedicated)
Activity