Implementation Guidelines

This document outlines the core design patterns and implementation guidelines for extending and maintaining the pyatlassian library. The library follows several key design patterns to ensure consistency, maintainability, and ease of use.

1. Source Code Organization Pattern

The pyatlassian library is organized using a hierarchical structure that reflects Atlassian’s unified API approach. Since Atlassian uses a single API endpoint for all its cloud services (Confluence, Jira, etc.), the codebase mirrors this architecture.

Python Library Folder Structure

pyatlassian/
├── atlassian/              # Base package with core functionality
│   ├── api.py              # Base API implementation
│   └── model.py            # Base Atlassian model
├── atlassian_confluence/   # Confluence-specific package
│   ├── api.py              # Confluence API implementation
│   ├── model.py            # Confluence model
│   └── utils.py            # Confluence utilities
└── atlassian_jira/         # Jira-specific package (planned)
    ├── api.py              # Jira API implementation
    ├── model.py            # Jira model
    └── utils.py            # Jira utilities
    ...

Base Atlassian Implementation

The base Atlassian class serves as the foundation for all product-specific implementations:

atlassian/model.py
  1# -*- coding: utf-8 -*-
  2
  3import typing as T
  4import json
  5import dataclasses
  6from functools import cached_property
  7
  8import requests
  9from requests.auth import HTTPBasicAuth
 10
 11from .exc import ParamError
 12from .arg import REQ, _REQUIRED, NA, rm_na, T_KWARGS
 13
 14T_RESPONSE = T.Dict[str, T.Any]
 15
 16
 17@dataclasses.dataclass
 18class BaseModel:
 19    def _validate(self):
 20        for field in dataclasses.fields(self.__class__):
 21            if field.init:
 22                k = field.name
 23                if getattr(self, k) is REQ:  # pragma: no cover
 24                    raise ParamError(f"Field {k!r} is required for {self.__class__}.")
 25
 26    def __post_init__(self):
 27        self._validate()
 28
 29    @classmethod
 30    def _split_req_opt(cls, kwargs: T_KWARGS) -> T.Tuple[T_KWARGS, T_KWARGS]:
 31        req_kwargs, opt_kwargs = dict(), dict()
 32        for field in dataclasses.fields(cls):
 33            if isinstance(field.default, _REQUIRED):
 34                try:
 35                    req_kwargs[field.name] = kwargs[field.name]
 36                except KeyError:
 37                    raise ParamError(
 38                        f"{field.name!r} is a required parameter for {cls}!"
 39                    )
 40            else:
 41                try:
 42                    opt_kwargs[field.name] = kwargs[field.name]
 43                except KeyError:
 44                    pass
 45        opt_kwargs = rm_na(**opt_kwargs)
 46        return req_kwargs, opt_kwargs
 47
 48
 49def _get_site_url(url: str) -> str:
 50    """
 51    Convert any of these url to https://mycompany.atlassian.net
 52
 53    - https://mycompany.atlassian.net/wiki/spaces/SPACEKEY/...
 54    - https://mycompany.atlassian.net/jira/core/projects/PROJECTKEY/board/...
 55    """
 56    parts = url.split("/")
 57    return "/".join(parts[:3])
 58
 59
 60@dataclasses.dataclass
 61class Atlassian(BaseModel):
 62    url: str = dataclasses.field(default=REQ)
 63    username: str = dataclasses.field(default=REQ)
 64    password: str = dataclasses.field(default=NA)
 65
 66    def __post_init__(self):
 67        self.url = _get_site_url(self.url)
 68
 69    @property
 70    def headers(self) -> T.Dict[str, T.Any]:
 71        return {"Content-Type": "application/json"}
 72
 73    @cached_property
 74    def http_basic_auth(self) -> "HTTPBasicAuth":
 75        return HTTPBasicAuth(username=self.username, password=self.password)
 76
 77    def request(
 78        self,
 79        method: str,  # GET, POST, PUT, DELETE
 80        url: str,
 81        params: T.Optional[T_KWARGS] = None,
 82        req_kwargs: T.Optional[T_KWARGS] = None,
 83    ) -> "requests.Response":
 84        """
 85        Make HTTP request and get response.
 86
 87        :param req_kwargs: additional ``requests.request()`` kwargs
 88        """
 89        kwargs = dict(
 90            method=method,
 91            url=url,
 92            headers=self.headers,
 93            params=params,
 94            auth=self.http_basic_auth,
 95        )
 96        if req_kwargs is not None:
 97            kwargs.update(req_kwargs)
 98        return requests.request(**kwargs)
 99
100    def make_request(
101        self,
102        method: str,
103        url: str,
104        params: T.Optional[T.Dict[str, T.Any]] = None,
105        req_kwargs: T.Optional[T_KWARGS] = None,
106    ) -> T_RESPONSE:
107        """
108        Wrap the response object with better error handling.
109
110        :param req_kwargs: additional ``requests.request()`` kwargs
111        """
112        # print(f"{method = }") # for debug only
113        # print(f"{url = }") # for debug only
114        # print(f"{params = }") # for debug only
115        res = self.request(
116            method=method,
117            url=url,
118            params=params,
119            req_kwargs=req_kwargs,
120        )
121        res.raise_for_status()
122        return json.loads(res.text)

Product-Specific Implementation

Each Atlassian product (like Confluence) extends the base class:

atlassian_confluence/model.py
 1# -*- coding: utf-8 -*-
 2
 3"""
 4"""
 5
 6import typing as T
 7import dataclasses
 8from functools import cached_property
 9
10from ..atlassian.api import (
11    Atlassian,
12    NA,
13    rm_na,
14    T_RESPONSE,
15    T_KWARGS,
16)
17
18from .children import ChildrenMixin
19from .label import LabelMixin
20from .page import PageMixin
21from .space import SpaceMixin
22
23
24@dataclasses.dataclass
25class Confluence(
26    Atlassian,
27    ChildrenMixin,
28    LabelMixin,
29    PageMixin,
30    SpaceMixin,
31):
32    """
33    - https://developer.atlassian.com/cloud/confluence/rest/v2/intro/#about
34    """
35
36    @cached_property
37    def _root_url(self) -> str:
38        return f"{self.url}/wiki/api/v2"

Utility Functions

Product-specific utilities are organized in dedicated modules:

atlassian_confluence/utils.py
 1# -*- coding: utf-8 -*-
 2
 3import typing as T
 4
 5
 6def extract_page_id_and_space_key_from_url(
 7    url: str,
 8) -> tuple[int, str]:
 9    """
10    Extract page id (123456) and space key (ABC) from the URL like this
11
12    - https://mycompany.atlassian.net/wiki/spaces/ABC/pages/edit-v2/123456
13    - https://mycompany.atlassian.net/wiki/spaces/ABC/pages/123456/This+Document+Is+Awesome
14    """
15    if url.startswith("https://") or url.startswith("http://"):
16        url = "/".join(url.split("/")[2:])
17    parts = url.split("/")
18    if len(parts) >= 7:
19        if parts[2] == "spaces" and parts[4] == "pages":
20            if parts[5] == "edit-v2":
21                return int(parts[6]), parts[3]
22            else:
23                return int(parts[5]), parts[3]
24        else:
25            raise NotImplementedError
26    else:
27        raise NotImplementedError

I’ll help expand the API Group Modularization Pattern section to clearly explain these relationships. Here’s a comprehensive write-up:

2. API Group Modularization Pattern

The library uses a mixin-based modularization pattern to organize API endpoints into logical groups that mirror Atlassian’s official API documentation structure.

API Group to Module Mapping

Each API group in Atlassian’s REST API (e.g., pages, spaces, comments) corresponds to a dedicated Python module containing a mixin class:

Atlassian API Structure          Python Implementation
────────────────────────         ───────────────────────
└── Confluence                   └── pyatlassian/atlassian_confluence/
    ├── Pages API Group              ├── page.py (PageMixin)
    └── Spaces API Group             └── space.py (SpaceMixin)

Mixin Implementation Pattern

Each API group module implements a mixin class that encapsulates all endpoints within that group:

atlassian_confluence/page.py
  1# -*- coding: utf-8 -*-
  2
  3"""
  4"""
  5
  6import typing as T
  7import dataclasses
  8
  9from ..pagi import _paginate
 10from ..atlassian.api import (
 11    NA,
 12    rm_na,
 13    T_RESPONSE,
 14    T_KWARGS,
 15)
 16from .typehint import T_BODY_FORMAT, T_PAGE_STATUS, T_PAGE_SORT_ORDER
 17
 18if T.TYPE_CHECKING:  # pragma: no cover
 19    from .model import Confluence
 20
 21
 22@dataclasses.dataclass
 23class PageMixin:
 24    """
 25    For detailed API document, see:
 26    https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-group-page
 27    """
 28
 29    def get_pages_for_label(
 30        self: "Confluence",
 31        id: int,
 32        space_id: list[int] = NA,
 33        body_format: T_BODY_FORMAT = NA,
 34        sort: T_PAGE_SORT_ORDER = NA,
 35        cursor: str = NA,
 36        limit: int = NA,
 37        req_kwargs: T.Optional[T_KWARGS] = None,
 38        _url: str = None,
 39    ) -> T_RESPONSE:
 40        """
 41        For detailed parameter descriptions, see:
 42        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-labels-id-pages-get
 43
 44        :param req_kwargs: additional ``requests.request()`` kwargs
 45        """
 46        if _url is None:
 47            url = f"{self._root_url}/labels/{id}/pages"
 48        else:
 49            url = _url
 50        params = {
 51            "space-id": space_id,
 52            "body-format": body_format,
 53            "sort": sort,
 54            "cursor": cursor,
 55            "limit": limit,
 56        }
 57        params = rm_na(**params)
 58        params = params if len(params) else None
 59        return self.make_request(
 60            method="GET",
 61            url=url,
 62            params=params,
 63            req_kwargs=req_kwargs,
 64        )
 65
 66    def pagi_get_pages_for_label(
 67        self: "Confluence",
 68        id: int,
 69        space_id: list[int] = NA,
 70        body_format: T_BODY_FORMAT = NA,
 71        sort: T_PAGE_SORT_ORDER = NA,
 72        cursor: str = NA,
 73        limit: int = NA,
 74        req_kwargs: T.Optional[T_KWARGS] = None,
 75        total_max_results: int = 9999,
 76    ) -> T.Iterable[T_RESPONSE]:
 77        """
 78        For detailed parameter descriptions, see:
 79        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-labels-id-pages-get
 80
 81        :param req_kwargs: additional ``requests.request()`` kwargs
 82        :param total_max_results: total max results to fetch in all response
 83        """
 84
 85        def get_next_token(res):
 86            return res.get("_links", {}).get("next")
 87
 88        def set_next_token(kwargs, next_token):
 89            kwargs["_url"] = f"{self.url}{next_token}"
 90
 91        yield from _paginate(
 92            method=self.get_pages_for_label,
 93            list_key="results",
 94            get_next_token=get_next_token,
 95            set_next_token=set_next_token,
 96            kwargs=dict(
 97                id=id,
 98                space_id=space_id,
 99                body_format=body_format,
100                sort=sort,
101                cursor=cursor,
102                limit=limit,
103                req_kwargs=req_kwargs,
104            ),
105            max_results=total_max_results,
106        )
107
108    def get_pages(
109        self: "Confluence",
110        id: list[int] = NA,
111        space_id: list[int] = NA,
112        sort: T_PAGE_SORT_ORDER = NA,
113        status: list[T_PAGE_STATUS] = NA,
114        title: str = NA,
115        body_format: T_BODY_FORMAT = NA,
116        cursor: str = NA,
117        limit: int = NA,
118        req_kwargs: T.Optional[T_KWARGS] = None,
119        _url: str = None,
120    ) -> T_RESPONSE:
121        """
122        For detailed parameter descriptions, see:
123        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-pages-get
124
125        :param paginate: If True, automatically handle pagination
126        :param max_results: Maximum number of total results to return
127            when ``paginate = True``
128        :param req_kwargs: additional ``requests.request()`` kwargs
129        """
130        if _url is None:
131            url = f"{self._root_url}/pages"
132        else:
133            url = _url
134        params = {
135            "id": id,
136            "space-id": space_id,
137            "sort": sort,
138            "status": status,
139            "title": title,
140            "body-format": body_format,
141            "cursor": cursor,
142            "limit": limit,
143        }
144        params = rm_na(**params)
145        params = params if len(params) else None
146        return self.make_request(
147            method="GET",
148            url=url,
149            params=params,
150            req_kwargs=req_kwargs,
151        )
152
153    def pagi_get_pages(
154        self: "Confluence",
155        id: list[int] = NA,
156        space_id: list[int] = NA,
157        sort: T_PAGE_SORT_ORDER = NA,
158        status: list[T_PAGE_STATUS] = NA,
159        title: str = NA,
160        body_format: T_BODY_FORMAT = NA,
161        cursor: str = NA,
162        limit: int = NA,
163        req_kwargs: T.Optional[T_KWARGS] = None,
164        total_max_results: int = 9999,
165    ) -> T.Iterable[T_RESPONSE]:
166        """
167        For detailed parameter descriptions, see:
168        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-pages-get
169
170        :param req_kwargs: additional ``requests.request()`` kwargs
171        :param total_max_results: total max results to fetch in all response
172        """
173
174        def get_next_token(res):
175            return res.get("_links", {}).get("next")
176
177        def set_next_token(kwargs, next_token):
178            kwargs["_url"] = f"{self.url}{next_token}"
179
180        yield from _paginate(
181            method=self.get_pages,
182            list_key="results",
183            get_next_token=get_next_token,
184            set_next_token=set_next_token,
185            kwargs=dict(
186                id=id,
187                space_id=space_id,
188                sort=sort,
189                status=status,
190                title=title,
191                body_format=body_format,
192                cursor=cursor,
193                limit=limit,
194                req_kwargs=req_kwargs,
195            ),
196            max_results=total_max_results,
197        )
198
199    def get_page_by_id(
200        self: "Confluence",
201        id: int,
202        body_format: T_BODY_FORMAT = NA,
203        get_draft: bool = NA,
204        status: list[
205            T.Literal[
206                "current",
207                "archived",
208                "trashed",
209                "deleted",
210                "historical",
211                "draft",
212            ]
213        ] = NA,
214        version: int = NA,
215        include_labels: bool = NA,
216        include_properties: bool = NA,
217        include_operations: bool = NA,
218        include_likes: bool = NA,
219        include_versions: bool = NA,
220        include_version: bool = NA,
221        include_favorited_by_current_user_status: bool = NA,
222        req_kwargs: T.Optional[T_KWARGS] = None,
223    ):
224        """
225        For detailed parameter descriptions, see:
226        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-pages-id-get
227
228        :param req_kwargs: additional ``requests.request()`` kwargs
229        """
230        params = {
231            "body-format": body_format,
232            "get-draft": get_draft,
233            "status": status,
234            "version": version,
235            "include-labels": include_labels,
236            "include-properties": include_properties,
237            "include-operations": include_operations,
238            "include-likes": include_likes,
239            "include-versions": include_versions,
240            "include-version": include_version,
241            "include-favorited-by-current-user-status": include_favorited_by_current_user_status,
242        }
243        params = rm_na(**params)
244        params = params if len(params) else None
245        return self.make_request(
246            method="GET",
247            url=f"{self._root_url}/pages/{id}",
248            params=params,
249            req_kwargs=req_kwargs,
250        )
251
252    def get_pages_in_space(
253        self: "Confluence",
254        id: int,
255        depth: str = NA,
256        sort: str = NA,
257        status: list[
258            T.Literal[
259                "current",
260                "archived",
261                "trashed",
262                "deleted",
263            ]
264        ] = NA,
265        title: str = NA,
266        body_format: T_BODY_FORMAT = NA,
267        cursor: str = NA,
268        limit: int = NA,
269        req_kwargs: T.Optional[T_KWARGS] = None,
270        _url: str = None,
271    ) -> T_RESPONSE:
272        """
273        For detailed parameter descriptions, see:
274        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-spaces-id-pages-get
275
276        :param req_kwargs: additional ``requests.request()`` kwargs
277        """
278        if _url is None:
279            url = f"{self._root_url}/spaces/{id}/pages"
280        else:
281            url = _url
282        params = {
283            "depth": depth,
284            "sort": sort,
285            "status": status,
286            "title": title,
287            "body-format": body_format,
288            "cursor": cursor,
289            "limit": limit,
290        }
291        params = rm_na(**params)
292        params = params if len(params) else None
293        return self.make_request(
294            method="GET",
295            url=url,
296            params=params,
297            req_kwargs=req_kwargs,
298        )
299
300    def pagi_get_pages_in_space(
301        self: "Confluence",
302        id: int,
303        depth: str = NA,
304        sort: str = NA,
305        status: list[
306            T.Literal[
307                "current",
308                "archived",
309                "trashed",
310                "deleted",
311            ]
312        ] = NA,
313        title: str = NA,
314        body_format: T_BODY_FORMAT = NA,
315        cursor: str = NA,
316        limit: int = NA,
317        req_kwargs: T.Optional[T_KWARGS] = None,
318        total_max_results: int = 9999,
319    ) -> T.Iterable[T_RESPONSE]:
320        """
321        For detailed parameter descriptions, see:
322        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-spaces-id-pages-get
323
324        :param req_kwargs: additional ``requests.request()`` kwargs
325        :param total_max_results: total max results to fetch in all response
326        """
327
328        def get_next_token(res):
329            return res.get("_links", {}).get("next")
330
331        def set_next_token(kwargs, next_token):
332            kwargs["_url"] = f"{self.url}{next_token}"
333
334        yield from _paginate(
335            method=self.get_pages_in_space,
336            list_key="results",
337            get_next_token=get_next_token,
338            set_next_token=set_next_token,
339            kwargs=dict(
340                id=id,
341                depth=depth,
342                sort=sort,
343                status=status,
344                title=title,
345                body_format=body_format,
346                cursor=cursor,
347                limit=limit,
348                req_kwargs=req_kwargs,
349            ),
350            max_results=total_max_results,
351        )

API Method to Python Method Mapping

Each REST API endpoint maps to a method within its corresponding mixin class:

REST API Endpoint                     Python Method
──────────────────                    ─────────────
GET /spaces/{id}/pages          →     get_pages_in_space()
GET /pages/{id}                 →     get_page_by_id()
POST /pages                     →     create_page()

The Complete Model

The main model class (e.g., Confluence) inherits from all relevant mixins to provide a unified API:

atlassian_confluence/model.py
 1# -*- coding: utf-8 -*-
 2
 3"""
 4"""
 5
 6import typing as T
 7import dataclasses
 8from functools import cached_property
 9
10from ..atlassian.api import (
11    Atlassian,
12    NA,
13    rm_na,
14    T_RESPONSE,
15    T_KWARGS,
16)
17
18from .children import ChildrenMixin
19from .label import LabelMixin
20from .page import PageMixin
21from .space import SpaceMixin
22
23
24@dataclasses.dataclass
25class Confluence(
26    Atlassian,
27    ChildrenMixin,
28    LabelMixin,
29    PageMixin,
30    SpaceMixin,
31):
32    """
33    - https://developer.atlassian.com/cloud/confluence/rest/v2/intro/#about
34    """
35
36    @cached_property
37    def _root_url(self) -> str:
38        return f"{self.url}/wiki/api/v2"

This pattern offers several benefits:

  1. Modularity: Each API group is self-contained and independently maintainable

  2. Organization: Clear mapping between Atlassian’s API documentation and code structure

  3. Extensibility: Easy to add new API groups or methods

  4. Reusability: Mixins can be combined flexibly for different use cases

  5. Maintainability: Changes to one API group don’t affect others

3. Method Naming Convention Pattern

The library follows a consistent method naming convention that converts Atlassian’s REST API endpoints into Pythonic method names while preserving semantics and readability.

Method Name Transformation Rules

Methods are named by slugifying the method names found in the official Atlassian documentation, not the API paths. This ensures semantic clarity and alignment with official documentation.

# Official Doc: "Get pages in space"
→ get_pages_in_space()

# Official Doc: "Get page by ID"
→ get_page_by_id()

Implementation Examples

atlassian_confluence/page.py
  1# -*- coding: utf-8 -*-
  2
  3"""
  4"""
  5
  6import typing as T
  7import dataclasses
  8
  9from ..pagi import _paginate
 10from ..atlassian.api import (
 11    NA,
 12    rm_na,
 13    T_RESPONSE,
 14    T_KWARGS,
 15)
 16from .typehint import T_BODY_FORMAT, T_PAGE_STATUS, T_PAGE_SORT_ORDER
 17
 18if T.TYPE_CHECKING:  # pragma: no cover
 19    from .model import Confluence
 20
 21
 22@dataclasses.dataclass
 23class PageMixin:
 24    """
 25    For detailed API document, see:
 26    https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-group-page
 27    """
 28
 29    def get_pages_for_label(
 30        self: "Confluence",
 31        id: int,
 32        space_id: list[int] = NA,
 33        body_format: T_BODY_FORMAT = NA,
 34        sort: T_PAGE_SORT_ORDER = NA,
 35        cursor: str = NA,
 36        limit: int = NA,
 37        req_kwargs: T.Optional[T_KWARGS] = None,
 38        _url: str = None,
 39    ) -> T_RESPONSE:
 40        """
 41        For detailed parameter descriptions, see:
 42        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-labels-id-pages-get
 43
 44        :param req_kwargs: additional ``requests.request()`` kwargs
 45        """
 46        if _url is None:
 47            url = f"{self._root_url}/labels/{id}/pages"
 48        else:
 49            url = _url
 50        params = {
 51            "space-id": space_id,
 52            "body-format": body_format,
 53            "sort": sort,
 54            "cursor": cursor,
 55            "limit": limit,
 56        }
 57        params = rm_na(**params)
 58        params = params if len(params) else None
 59        return self.make_request(
 60            method="GET",
 61            url=url,
 62            params=params,
 63            req_kwargs=req_kwargs,
 64        )
 65
 66    def pagi_get_pages_for_label(
 67        self: "Confluence",
 68        id: int,
 69        space_id: list[int] = NA,
 70        body_format: T_BODY_FORMAT = NA,
 71        sort: T_PAGE_SORT_ORDER = NA,
 72        cursor: str = NA,
 73        limit: int = NA,
 74        req_kwargs: T.Optional[T_KWARGS] = None,
 75        total_max_results: int = 9999,
 76    ) -> T.Iterable[T_RESPONSE]:
 77        """
 78        For detailed parameter descriptions, see:
 79        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-labels-id-pages-get
 80
 81        :param req_kwargs: additional ``requests.request()`` kwargs
 82        :param total_max_results: total max results to fetch in all response
 83        """
 84
 85        def get_next_token(res):
 86            return res.get("_links", {}).get("next")
 87
 88        def set_next_token(kwargs, next_token):
 89            kwargs["_url"] = f"{self.url}{next_token}"
 90
 91        yield from _paginate(
 92            method=self.get_pages_for_label,
 93            list_key="results",
 94            get_next_token=get_next_token,
 95            set_next_token=set_next_token,
 96            kwargs=dict(
 97                id=id,
 98                space_id=space_id,
 99                body_format=body_format,
100                sort=sort,
101                cursor=cursor,
102                limit=limit,
103                req_kwargs=req_kwargs,
104            ),
105            max_results=total_max_results,
106        )
107
108    def get_pages(
109        self: "Confluence",
110        id: list[int] = NA,
111        space_id: list[int] = NA,
112        sort: T_PAGE_SORT_ORDER = NA,
113        status: list[T_PAGE_STATUS] = NA,
114        title: str = NA,
115        body_format: T_BODY_FORMAT = NA,
116        cursor: str = NA,
117        limit: int = NA,
118        req_kwargs: T.Optional[T_KWARGS] = None,
119        _url: str = None,
120    ) -> T_RESPONSE:
121        """
122        For detailed parameter descriptions, see:
123        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-pages-get
124
125        :param paginate: If True, automatically handle pagination
126        :param max_results: Maximum number of total results to return
127            when ``paginate = True``
128        :param req_kwargs: additional ``requests.request()`` kwargs
129        """
130        if _url is None:
131            url = f"{self._root_url}/pages"
132        else:
133            url = _url
134        params = {
135            "id": id,
136            "space-id": space_id,
137            "sort": sort,
138            "status": status,
139            "title": title,
140            "body-format": body_format,
141            "cursor": cursor,
142            "limit": limit,
143        }
144        params = rm_na(**params)
145        params = params if len(params) else None
146        return self.make_request(
147            method="GET",
148            url=url,
149            params=params,
150            req_kwargs=req_kwargs,
151        )
152
153    def pagi_get_pages(
154        self: "Confluence",
155        id: list[int] = NA,
156        space_id: list[int] = NA,
157        sort: T_PAGE_SORT_ORDER = NA,
158        status: list[T_PAGE_STATUS] = NA,
159        title: str = NA,
160        body_format: T_BODY_FORMAT = NA,
161        cursor: str = NA,
162        limit: int = NA,
163        req_kwargs: T.Optional[T_KWARGS] = None,
164        total_max_results: int = 9999,
165    ) -> T.Iterable[T_RESPONSE]:
166        """
167        For detailed parameter descriptions, see:
168        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-pages-get
169
170        :param req_kwargs: additional ``requests.request()`` kwargs
171        :param total_max_results: total max results to fetch in all response
172        """
173
174        def get_next_token(res):
175            return res.get("_links", {}).get("next")
176
177        def set_next_token(kwargs, next_token):
178            kwargs["_url"] = f"{self.url}{next_token}"
179
180        yield from _paginate(
181            method=self.get_pages,
182            list_key="results",
183            get_next_token=get_next_token,
184            set_next_token=set_next_token,
185            kwargs=dict(
186                id=id,
187                space_id=space_id,
188                sort=sort,
189                status=status,
190                title=title,
191                body_format=body_format,
192                cursor=cursor,
193                limit=limit,
194                req_kwargs=req_kwargs,
195            ),
196            max_results=total_max_results,
197        )
198
199    def get_page_by_id(
200        self: "Confluence",
201        id: int,
202        body_format: T_BODY_FORMAT = NA,
203        get_draft: bool = NA,
204        status: list[
205            T.Literal[
206                "current",
207                "archived",
208                "trashed",
209                "deleted",
210                "historical",
211                "draft",
212            ]
213        ] = NA,
214        version: int = NA,
215        include_labels: bool = NA,
216        include_properties: bool = NA,
217        include_operations: bool = NA,
218        include_likes: bool = NA,
219        include_versions: bool = NA,
220        include_version: bool = NA,
221        include_favorited_by_current_user_status: bool = NA,
222        req_kwargs: T.Optional[T_KWARGS] = None,
223    ):
224        """
225        For detailed parameter descriptions, see:
226        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-pages-id-get
227
228        :param req_kwargs: additional ``requests.request()`` kwargs
229        """
230        params = {
231            "body-format": body_format,
232            "get-draft": get_draft,
233            "status": status,
234            "version": version,
235            "include-labels": include_labels,
236            "include-properties": include_properties,
237            "include-operations": include_operations,
238            "include-likes": include_likes,
239            "include-versions": include_versions,
240            "include-version": include_version,
241            "include-favorited-by-current-user-status": include_favorited_by_current_user_status,
242        }
243        params = rm_na(**params)
244        params = params if len(params) else None
245        return self.make_request(
246            method="GET",
247            url=f"{self._root_url}/pages/{id}",
248            params=params,
249            req_kwargs=req_kwargs,
250        )
251
252    def get_pages_in_space(
253        self: "Confluence",
254        id: int,
255        depth: str = NA,
256        sort: str = NA,
257        status: list[
258            T.Literal[
259                "current",
260                "archived",
261                "trashed",
262                "deleted",
263            ]
264        ] = NA,
265        title: str = NA,
266        body_format: T_BODY_FORMAT = NA,
267        cursor: str = NA,
268        limit: int = NA,
269        req_kwargs: T.Optional[T_KWARGS] = None,
270        _url: str = None,
271    ) -> T_RESPONSE:
272        """
273        For detailed parameter descriptions, see:
274        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-spaces-id-pages-get
275
276        :param req_kwargs: additional ``requests.request()`` kwargs
277        """
278        if _url is None:
279            url = f"{self._root_url}/spaces/{id}/pages"
280        else:
281            url = _url
282        params = {
283            "depth": depth,
284            "sort": sort,
285            "status": status,
286            "title": title,
287            "body-format": body_format,
288            "cursor": cursor,
289            "limit": limit,
290        }
291        params = rm_na(**params)
292        params = params if len(params) else None
293        return self.make_request(
294            method="GET",
295            url=url,
296            params=params,
297            req_kwargs=req_kwargs,
298        )
299
300    def pagi_get_pages_in_space(
301        self: "Confluence",
302        id: int,
303        depth: str = NA,
304        sort: str = NA,
305        status: list[
306            T.Literal[
307                "current",
308                "archived",
309                "trashed",
310                "deleted",
311            ]
312        ] = NA,
313        title: str = NA,
314        body_format: T_BODY_FORMAT = NA,
315        cursor: str = NA,
316        limit: int = NA,
317        req_kwargs: T.Optional[T_KWARGS] = None,
318        total_max_results: int = 9999,
319    ) -> T.Iterable[T_RESPONSE]:
320        """
321        For detailed parameter descriptions, see:
322        https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-spaces-id-pages-get
323
324        :param req_kwargs: additional ``requests.request()`` kwargs
325        :param total_max_results: total max results to fetch in all response
326        """
327
328        def get_next_token(res):
329            return res.get("_links", {}).get("next")
330
331        def set_next_token(kwargs, next_token):
332            kwargs["_url"] = f"{self.url}{next_token}"
333
334        yield from _paginate(
335            method=self.get_pages_in_space,
336            list_key="results",
337            get_next_token=get_next_token,
338            set_next_token=set_next_token,
339            kwargs=dict(
340                id=id,
341                depth=depth,
342                sort=sort,
343                status=status,
344                title=title,
345                body_format=body_format,
346                cursor=cursor,
347                limit=limit,
348                req_kwargs=req_kwargs,
349            ),
350            max_results=total_max_results,
351        )

Parameter Naming Rules

  1. Path Parameters

# API: /spaces/{id}/pages
def get_pages_in_space(
    self,
    id: int,
    ...
):  # 'id' matches path parameter
    pass
  1. Query Parameters

    # API: ?body-format=storage&get-draft=true
    def get_page_by_id(
        self,
        id: int,
        body_format: str = NA,  # hyphen → underscore
        get_draft: bool = NA,   # get-draft → get_draft
        ...
    ):
        ...
    

4. Parameter Handling Pattern

The library makes a clear distinction between required and optional parameters using Python’s type system and special singleton objects. Required parameters are enforced at initialization, while optional parameters can be omitted.

def get_pages_in_space(
    self,
    id: int,              # Required: no default value
    depth: str = NA,      # Optional: defaults to NA
    sort: str = NA,       # Optional: defaults to NA
    status: list = NA,    # Optional: defaults to NA
    ...
):
    params = {
        "depth": depth,
        "sort": sort,
        "status": status,
    }
    # Remove NA parameters before making request
    params = rm_na(params)
    params = params if len(params) else None
    res = self.make_request(
        method="GET",
        url=_url,
        params=params,
    )

Key Concepts:

  • Required Parameters
    • Must be provided by the caller

    • No default value assigned

    • Enforced through Python’s argument system

    • Example: id

  • Optional Parameters
    • Can be omitted by the caller

    • Default value is NA singleton

    • Automatically filtered out using rm_na

    • Example: limit, sort, status

5. Pagination Implementation Pattern

The library implements a consistent pagination pattern for API endpoints that return paginated results. This pattern handles both manual and automatic pagination while maintaining a clean interface.

The pagination implementation uses recursive calls with accumulation, controlled by specific pagination parameters:

  1. Control Parameters
    • paginate: bool = False - Enables/disables automatic pagination

    • max_results: int = 9999 - Maximum number of results to return

    • _url: str = None - Internal parameter for continuation URLs

    • _results: list = None - Internal parameter for result accumulation

  2. Base URL and Results Initia lization

if _url is None:
    _url = f"{self._root_url}/spaces/{id}/pages"
if _results is None:
    _results = []
if len(_results) >= max_results:
    return {"results": _results}
  1. Result Accumulation

_results.extend(res.get("results", []))
  1. Pagination Control Flow

if "next" in res["_links"] and paginate:
    _url = f"{self.url}{res['_links']['next']}"
    _res = self.get_pages_in_space(
        # ... pass through all original parameters ...
        _url=_url,
        _results=_results,
    )

Key Benefits

  1. Transparency: Users can choose between single-page and automatic pagination

  2. Control: max_results parameter prevents unbounded result sets

  3. Efficiency: Results are accumulated incrementally

  4. Consistency: Same method signature works for both paginated and non-paginated requests

  5. Clean Interface: Internal pagination parameters are hidden from API documentation

Implementation Notes

  • Internal parameters (_url, _results) should not be exposed in public documentation

  • Result accumulation preserves the original API response structure

  • Error handling and rate limiting should be considered in the implementation