Skip to content

Commit 8b81956

Browse files
committed
refactor: reorganize Confluence module into proper directory structure
1 parent 9e17480 commit 8b81956

File tree

8 files changed

+342
-3886
lines changed

8 files changed

+342
-3886
lines changed

atlassian/confluence.py

Lines changed: 0 additions & 3877 deletions
This file was deleted.

atlassian/confluence/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""
2+
Confluence module for both Cloud and Server implementations
3+
"""
4+
from atlassian.confluence.base import ConfluenceBase
5+
from atlassian.confluence.cloud import ConfluenceCloud
6+
from atlassian.confluence.server import ConfluenceServer
7+
8+
__all__ = ['ConfluenceBase', 'ConfluenceCloud', 'ConfluenceServer']

atlassian/confluence/base.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
"""
2+
Confluence base module for shared functionality between API versions
3+
"""
4+
import logging
5+
from typing import Dict, List, Optional, Union, Any, Tuple
6+
from urllib.parse import urlparse
7+
8+
from atlassian.rest_client import AtlassianRestAPI
9+
10+
log = logging.getLogger(__name__)
11+
12+
13+
class ConfluenceEndpoints:
14+
"""
15+
Class to define endpoint mappings for different Confluence API versions.
16+
These endpoints can be accessed through the ConfluenceBase get_endpoint method.
17+
"""
18+
V1 = {
19+
"page": "rest/api/content",
20+
"page_by_id": "rest/api/content/{id}",
21+
"child_pages": "rest/api/content/{id}/child/page",
22+
"content_search": "rest/api/content/search",
23+
"space": "rest/api/space",
24+
"space_by_key": "rest/api/space/{key}",
25+
}
26+
27+
V2 = {
28+
'page_by_id': 'api/v2/pages/{id}',
29+
'page': 'api/v2/pages',
30+
'child_pages': 'api/v2/pages/{id}/children/page',
31+
'search': 'api/v2/search',
32+
'spaces': 'api/v2/spaces',
33+
'space_by_id': 'api/v2/spaces/{id}',
34+
'page_properties': 'api/v2/pages/{id}/properties',
35+
'page_property_by_key': 'api/v2/pages/{id}/properties/{key}',
36+
'page_labels': 'api/v2/pages/{id}/labels',
37+
'space_labels': 'api/v2/spaces/{id}/labels',
38+
39+
# Comment endpoints for V2 API
40+
'page_footer_comments': 'api/v2/pages/{id}/footer-comments',
41+
'page_inline_comments': 'api/v2/pages/{id}/inline-comments',
42+
'blogpost_footer_comments': 'api/v2/blogposts/{id}/footer-comments',
43+
'blogpost_inline_comments': 'api/v2/blogposts/{id}/inline-comments',
44+
'attachment_comments': 'api/v2/attachments/{id}/footer-comments',
45+
'custom_content_comments': 'api/v2/custom-content/{id}/footer-comments',
46+
'comment': 'api/v2/comments',
47+
'comment_by_id': 'api/v2/comments/{id}',
48+
'comment_children': 'api/v2/comments/{id}/children',
49+
50+
# Whiteboard endpoints
51+
'whiteboard': 'api/v2/whiteboards',
52+
'whiteboard_by_id': 'api/v2/whiteboards/{id}',
53+
'whiteboard_children': 'api/v2/whiteboards/{id}/children',
54+
'whiteboard_ancestors': 'api/v2/whiteboards/{id}/ancestors',
55+
56+
# Custom content endpoints
57+
'custom_content': 'api/v2/custom-content',
58+
'custom_content_by_id': 'api/v2/custom-content/{id}',
59+
'custom_content_children': 'api/v2/custom-content/{id}/children',
60+
'custom_content_ancestors': 'api/v2/custom-content/{id}/ancestors',
61+
'custom_content_labels': 'api/v2/custom-content/{id}/labels',
62+
'custom_content_properties': 'api/v2/custom-content/{id}/properties',
63+
'custom_content_property_by_key': 'api/v2/custom-content/{id}/properties/{key}',
64+
65+
# More v2 endpoints will be added in Phase 2 and 3
66+
}
67+
68+
69+
class ConfluenceBase(AtlassianRestAPI):
70+
"""Base class for Confluence operations with version support"""
71+
72+
@staticmethod
73+
def _is_cloud_url(url: str) -> bool:
74+
"""
75+
Securely validate if a URL is a Confluence Cloud URL.
76+
77+
Args:
78+
url: The URL to validate
79+
80+
Returns:
81+
bool: True if the URL is a valid Confluence Cloud URL
82+
"""
83+
parsed = urlparse(url)
84+
# Ensure we have a valid URL with a hostname
85+
if not parsed.hostname:
86+
return False
87+
88+
# Check if the hostname ends with .atlassian.net or .jira.com
89+
hostname = parsed.hostname.lower()
90+
return hostname.endswith('.atlassian.net') or hostname.endswith('.jira.com')
91+
92+
def __init__(
93+
self,
94+
url: str,
95+
*args,
96+
api_version: Union[str, int] = 1,
97+
**kwargs
98+
):
99+
"""
100+
Initialize the Confluence Base instance with version support.
101+
102+
Args:
103+
url: The Confluence instance URL
104+
api_version: API version, 1 or 2, defaults to 1
105+
args: Arguments to pass to AtlassianRestAPI constructor
106+
kwargs: Keyword arguments to pass to AtlassianRestAPI constructor
107+
"""
108+
if self._is_cloud_url(url) and "/wiki" not in url:
109+
url = AtlassianRestAPI.url_joiner(url, "/wiki")
110+
if "cloud" not in kwargs:
111+
kwargs["cloud"] = True
112+
113+
super(ConfluenceBase, self).__init__(url, *args, **kwargs)
114+
self.api_version = int(api_version)
115+
if self.api_version not in [1, 2]:
116+
raise ValueError("API version must be 1 or 2")
117+
118+
def get_endpoint(self, endpoint_key: str, **kwargs) -> str:
119+
"""
120+
Get the appropriate endpoint based on the API version.
121+
122+
Args:
123+
endpoint_key: The key for the endpoint in the endpoints dictionary
124+
kwargs: Format parameters for the endpoint
125+
126+
Returns:
127+
The formatted endpoint URL
128+
"""
129+
endpoints = ConfluenceEndpoints.V1 if self.api_version == 1 else ConfluenceEndpoints.V2
130+
131+
if endpoint_key not in endpoints:
132+
raise ValueError(f"Endpoint key '{endpoint_key}' not found for API version {self.api_version}")
133+
134+
endpoint = endpoints[endpoint_key]
135+
136+
# Format the endpoint if kwargs are provided
137+
if kwargs:
138+
endpoint = endpoint.format(**kwargs)
139+
140+
return endpoint
141+
142+
def _get_paged(
143+
self,
144+
url: str,
145+
params: Optional[Dict] = None,
146+
data: Optional[Dict] = None,
147+
flags: Optional[List] = None,
148+
trailing: Optional[bool] = None,
149+
absolute: bool = False,
150+
):
151+
"""
152+
Get paged results with version-appropriate pagination.
153+
154+
Args:
155+
url: The URL to retrieve
156+
params: The query parameters
157+
data: The request data
158+
flags: Additional flags
159+
trailing: If True, a trailing slash is added to the URL
160+
absolute: If True, the URL is used absolute and not relative to the root
161+
162+
Yields:
163+
The result elements
164+
"""
165+
if params is None:
166+
params = {}
167+
168+
if self.api_version == 1:
169+
# V1 API pagination (offset-based)
170+
while True:
171+
response = self.get(
172+
url,
173+
trailing=trailing,
174+
params=params,
175+
data=data,
176+
flags=flags,
177+
absolute=absolute,
178+
)
179+
if "results" not in response:
180+
return
181+
182+
for value in response.get("results", []):
183+
yield value
184+
185+
# According to Cloud and Server documentation the links are returned the same way:
186+
# https://developer.atlassian.com/cloud/confluence/rest/api-group-content/#api-wiki-rest-api-content-get
187+
# https://developer.atlassian.com/server/confluence/pagination-in-the-rest-api/
188+
url = response.get("_links", {}).get("next")
189+
if url is None:
190+
break
191+
# From now on we have relative URLs with parameters
192+
absolute = False
193+
# Params are now provided by the url
194+
params = {}
195+
# Trailing should not be added as it is already part of the url
196+
trailing = False
197+
198+
else:
199+
# V2 API pagination (cursor-based)
200+
while True:
201+
response = self.get(
202+
url,
203+
trailing=trailing,
204+
params=params,
205+
data=data,
206+
flags=flags,
207+
absolute=absolute,
208+
)
209+
210+
if "results" not in response:
211+
return
212+
213+
for value in response.get("results", []):
214+
yield value
215+
216+
# Check for next cursor in _links or in response headers
217+
next_url = response.get("_links", {}).get("next")
218+
219+
if not next_url:
220+
# Check for Link header
221+
if hasattr(self, "response") and self.response and "Link" in self.response.headers:
222+
link_header = self.response.headers["Link"]
223+
if 'rel="next"' in link_header:
224+
import re
225+
match = re.search(r'<([^>]*)>;', link_header)
226+
if match:
227+
next_url = match.group(1)
228+
229+
if not next_url:
230+
break
231+
232+
# Use the next URL directly
233+
# Check if the response has a base URL provided (common in Confluence v2 API)
234+
base_url = response.get("_links", {}).get("base")
235+
if base_url and next_url.startswith('/'):
236+
# Construct the full URL using the base URL from the response
237+
url = f"{base_url}{next_url}"
238+
absolute = True
239+
else:
240+
url = next_url
241+
# Check if the URL is absolute (has http:// or https://) or contains the server's domain
242+
if next_url.startswith(('http://', 'https://')) or self.url.split('/')[2] in next_url:
243+
absolute = True
244+
else:
245+
absolute = False
246+
params = {}
247+
trailing = False
248+
249+
return
250+
251+
@staticmethod
252+
def factory(url: str, api_version: int = 1, *args, **kwargs) -> 'ConfluenceBase':
253+
"""
254+
Factory method to create a Confluence client with the specified API version
255+
256+
Args:
257+
url: Confluence Cloud base URL
258+
api_version: API version to use (1 or 2)
259+
*args: Variable length argument list
260+
**kwargs: Keyword arguments
261+
262+
Returns:
263+
Configured Confluence client for the specified API version
264+
265+
Raises:
266+
ValueError: If api_version is not 1 or 2
267+
"""
268+
if api_version == 1:
269+
from .confluence import Confluence
270+
return Confluence(url, *args, **kwargs)
271+
elif api_version == 2:
272+
from .confluence_v2 import ConfluenceV2
273+
return ConfluenceV2(url, *args, **kwargs)
274+
else:
275+
raise ValueError(f"Unsupported API version: {api_version}. Use 1 or 2.")

atlassian/confluence/cloud/__init__.py

Whitespace-only changes.

atlassian/confluence_v2.py renamed to atlassian/confluence/cloud/confluence_cloud_v2.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# -*- coding: utf-8 -*-
33

44
"""
5-
Module for Confluence API v2 implementation
5+
Module for Confluence Cloud API v2 implementation
66
"""
77

88
import logging
@@ -11,19 +11,19 @@
1111

1212
from typing import Dict, List, Optional, Union, Any
1313

14-
from .confluence_base import ConfluenceBase
14+
from ..base import ConfluenceBase
1515

1616
log = logging.getLogger(__name__)
1717

1818

19-
class ConfluenceV2(ConfluenceBase):
19+
class ConfluenceCloud(ConfluenceBase):
2020
"""
21-
Confluence API v2 implementation class
21+
Confluence Cloud API v2 implementation class
2222
"""
2323

2424
def __init__(self, url: str, *args, **kwargs):
2525
"""
26-
Initialize the ConfluenceV2 instance with API version 2
26+
Initialize the ConfluenceCloud instance with API version 2
2727
2828
Args:
2929
url: Confluence Cloud base URL
@@ -35,13 +35,13 @@ def __init__(self, url: str, *args, **kwargs):
3535

3636
# Check if the URL already contains '/wiki'
3737
# This prevents a double '/wiki/wiki' issue when the parent class adds it again
38-
if ("atlassian.net" in url or "jira.com" in url) and ("/wiki" in url):
38+
if self._is_cloud_url(url) and "/wiki" in url:
3939
# Remove the '/wiki' suffix since the parent class will add it
4040
url = url.rstrip("/")
4141
if url.endswith("/wiki"):
4242
url = url[:-5]
4343

44-
super(ConfluenceV2, self).__init__(url, *args, **kwargs)
44+
super(ConfluenceCloud, self).__init__(url, *args, **kwargs)
4545
self._compatibility_method_mapping = {
4646
# V1 method => V2 method mapping
4747
"get_content": "get_pages",
@@ -82,7 +82,7 @@ def __getattr__(self, name):
8282
@functools.wraps(v2_method)
8383
def compatibility_wrapper(*args, **kwargs):
8484
warnings.warn(
85-
f"The method '{name}' is deprecated in ConfluenceV2. "
85+
f"The method '{name}' is deprecated in ConfluenceCloud. "
8686
f"Use '{v2_method_name}' instead.",
8787
DeprecationWarning,
8888
stacklevel=2

atlassian/confluence/server/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Module for Confluence Server API implementation
3+
"""
4+
5+
import logging
6+
from typing import Dict, List, Optional, Union, Any
7+
8+
from ..base import ConfluenceBase
9+
10+
log = logging.getLogger(__name__)
11+
12+
13+
class ConfluenceServer(ConfluenceBase):
14+
"""
15+
Confluence Server API implementation class
16+
"""
17+
18+
def __init__(self, url: str, *args, **kwargs):
19+
"""
20+
Initialize the ConfluenceServer instance
21+
22+
Args:
23+
url: Confluence Server base URL
24+
*args: Variable length argument list passed to ConfluenceBase
25+
**kwargs: Keyword arguments passed to ConfluenceBase
26+
"""
27+
# Server only supports v1
28+
kwargs.setdefault('api_version', 1)
29+
super(ConfluenceServer, self).__init__(url, *args, **kwargs)

0 commit comments

Comments
 (0)