Skip to content

Commit 069d82c

Browse files
committed
Implement Phase 1: Core Structure for Confluence API v2 support
1 parent ee75535 commit 069d82c

File tree

7 files changed

+517
-22
lines changed

7 files changed

+517
-22
lines changed

atlassian/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from .bitbucket import Bitbucket as Stash
44
from .cloud_admin import CloudAdminOrgs, CloudAdminUsers
55
from .confluence import Confluence
6+
from .confluence_base import ConfluenceBase
7+
from .confluence_v2 import ConfluenceV2
68
from .crowd import Crowd
79
from .insight import Insight
810
from .insight import Insight as Assets
@@ -13,8 +15,29 @@
1315
from .service_desk import ServiceDesk as ServiceManagement
1416
from .xray import Xray
1517

18+
19+
# Factory function for Confluence client
20+
def create_confluence(url, *args, api_version=1, **kwargs):
21+
"""
22+
Create a Confluence client with the specified API version.
23+
24+
Args:
25+
url: The Confluence instance URL
26+
api_version: API version, 1 or 2, defaults to 1
27+
args: Arguments to pass to Confluence constructor
28+
kwargs: Keyword arguments to pass to Confluence constructor
29+
30+
Returns:
31+
A Confluence client configured for the specified API version
32+
"""
33+
return ConfluenceBase.factory(url, *args, api_version=api_version, **kwargs)
34+
35+
1636
__all__ = [
1737
"Confluence",
38+
"ConfluenceBase",
39+
"ConfluenceV2",
40+
"create_confluence",
1841
"Jira",
1942
"Bitbucket",
2043
"CloudAdminOrgs",

atlassian/confluence.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
ApiPermissionError,
2323
ApiValueError,
2424
)
25-
from .rest_client import AtlassianRestAPI
25+
from .confluence_base import ConfluenceBase
2626

2727
log = logging.getLogger(__name__)
2828

2929

30-
class Confluence(AtlassianRestAPI):
30+
class Confluence(ConfluenceBase):
3131
content_types = {
3232
".gif": "image/gif",
3333
".png": "image/png",
@@ -40,10 +40,8 @@ class Confluence(AtlassianRestAPI):
4040
}
4141

4242
def __init__(self, url, *args, **kwargs):
43-
if ("atlassian.net" in url or "jira.com" in url) and ("/wiki" not in url):
44-
url = AtlassianRestAPI.url_joiner(url, "/wiki")
45-
if "cloud" not in kwargs:
46-
kwargs["cloud"] = True
43+
# Set default API version to 1 for backward compatibility
44+
kwargs.setdefault('api_version', 1)
4745
super(Confluence, self).__init__(url, *args, **kwargs)
4846

4947
@staticmethod

atlassian/confluence_base.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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+
7+
from atlassian.rest_client import AtlassianRestAPI
8+
9+
log = logging.getLogger(__name__)
10+
11+
12+
class ConfluenceEndpoints:
13+
"""Class for storing Confluence endpoints for different API versions"""
14+
15+
V1 = {
16+
"page": "rest/api/content",
17+
"page_by_id": "rest/api/content/{id}",
18+
"child_pages": "rest/api/content/{id}/child/page",
19+
"content_search": "rest/api/content/search",
20+
"space": "rest/api/space",
21+
"space_by_key": "rest/api/space/{key}",
22+
}
23+
24+
V2 = {
25+
"page": "api/v2/pages",
26+
"page_by_id": "api/v2/pages/{id}",
27+
"child_pages": "api/v2/pages/{id}/children",
28+
"content_search": "api/v2/search",
29+
"space": "api/v2/spaces",
30+
"space_by_key": "api/v2/spaces/{key}",
31+
}
32+
33+
34+
class ConfluenceBase(AtlassianRestAPI):
35+
"""Base class for Confluence operations with version support"""
36+
37+
def __init__(
38+
self,
39+
url: str,
40+
*args,
41+
api_version: Union[str, int] = 1,
42+
**kwargs
43+
):
44+
"""
45+
Initialize the Confluence Base instance with version support.
46+
47+
Args:
48+
url: The Confluence instance URL
49+
api_version: API version, 1 or 2, defaults to 1
50+
args: Arguments to pass to AtlassianRestAPI constructor
51+
kwargs: Keyword arguments to pass to AtlassianRestAPI constructor
52+
"""
53+
if ("atlassian.net" in url or "jira.com" in url) and ("/wiki" not in url):
54+
url = AtlassianRestAPI.url_joiner(url, "/wiki")
55+
if "cloud" not in kwargs:
56+
kwargs["cloud"] = True
57+
58+
super(ConfluenceBase, self).__init__(url, *args, **kwargs)
59+
self.api_version = int(api_version)
60+
if self.api_version not in [1, 2]:
61+
raise ValueError("API version must be 1 or 2")
62+
63+
def get_endpoint(self, endpoint_key: str, **kwargs) -> str:
64+
"""
65+
Get the appropriate endpoint based on the API version.
66+
67+
Args:
68+
endpoint_key: The key for the endpoint in the endpoints dictionary
69+
kwargs: Format parameters for the endpoint
70+
71+
Returns:
72+
The formatted endpoint URL
73+
"""
74+
endpoints = ConfluenceEndpoints.V1 if self.api_version == 1 else ConfluenceEndpoints.V2
75+
76+
if endpoint_key not in endpoints:
77+
raise ValueError(f"Endpoint key '{endpoint_key}' not found for API version {self.api_version}")
78+
79+
endpoint = endpoints[endpoint_key]
80+
81+
# Format the endpoint if kwargs are provided
82+
if kwargs:
83+
endpoint = endpoint.format(**kwargs)
84+
85+
return endpoint
86+
87+
def _get_paged(
88+
self,
89+
url: str,
90+
params: Optional[Dict] = None,
91+
data: Optional[Dict] = None,
92+
flags: Optional[List] = None,
93+
trailing: Optional[bool] = None,
94+
absolute: bool = False,
95+
):
96+
"""
97+
Get paged results with version-appropriate pagination.
98+
99+
Args:
100+
url: The URL to retrieve
101+
params: The query parameters
102+
data: The request data
103+
flags: Additional flags
104+
trailing: If True, a trailing slash is added to the URL
105+
absolute: If True, the URL is used absolute and not relative to the root
106+
107+
Yields:
108+
The result elements
109+
"""
110+
if params is None:
111+
params = {}
112+
113+
if self.api_version == 1:
114+
# V1 API pagination (offset-based)
115+
while True:
116+
response = self.get(
117+
url,
118+
trailing=trailing,
119+
params=params,
120+
data=data,
121+
flags=flags,
122+
absolute=absolute,
123+
)
124+
if "results" not in response:
125+
return
126+
127+
for value in response.get("results", []):
128+
yield value
129+
130+
# According to Cloud and Server documentation the links are returned the same way:
131+
# https://developer.atlassian.com/cloud/confluence/rest/api-group-content/#api-wiki-rest-api-content-get
132+
# https://developer.atlassian.com/server/confluence/pagination-in-the-rest-api/
133+
url = response.get("_links", {}).get("next")
134+
if url is None:
135+
break
136+
# From now on we have relative URLs with parameters
137+
absolute = False
138+
# Params are now provided by the url
139+
params = {}
140+
# Trailing should not be added as it is already part of the url
141+
trailing = False
142+
143+
else:
144+
# V2 API pagination (cursor-based)
145+
while True:
146+
response = self.get(
147+
url,
148+
trailing=trailing,
149+
params=params,
150+
data=data,
151+
flags=flags,
152+
absolute=absolute,
153+
)
154+
155+
if "results" not in response:
156+
return
157+
158+
for value in response.get("results", []):
159+
yield value
160+
161+
# Check for next cursor in _links or in response headers
162+
next_url = response.get("_links", {}).get("next")
163+
164+
if not next_url:
165+
# Check for Link header
166+
if hasattr(self, "response") and self.response and "Link" in self.response.headers:
167+
link_header = self.response.headers["Link"]
168+
if 'rel="next"' in link_header:
169+
import re
170+
match = re.search(r'<([^>]*)>;', link_header)
171+
if match:
172+
next_url = match.group(1)
173+
174+
if not next_url:
175+
break
176+
177+
# Use the next URL directly
178+
url = next_url
179+
absolute = False
180+
params = {}
181+
trailing = False
182+
183+
return
184+
185+
@staticmethod
186+
def factory(url: str, api_version: int = 1, *args, **kwargs) -> 'ConfluenceBase':
187+
"""
188+
Factory method to create a Confluence client with the specified API version
189+
190+
Args:
191+
url: Confluence Cloud base URL
192+
api_version: API version to use (1 or 2)
193+
*args: Variable length argument list
194+
**kwargs: Keyword arguments
195+
196+
Returns:
197+
Configured Confluence client for the specified API version
198+
199+
Raises:
200+
ValueError: If api_version is not 1 or 2
201+
"""
202+
if api_version == 1:
203+
from .confluence import Confluence
204+
return Confluence(url, *args, **kwargs)
205+
elif api_version == 2:
206+
from .confluence_v2 import ConfluenceV2
207+
return ConfluenceV2(url, *args, **kwargs)
208+
else:
209+
raise ValueError(f"Unsupported API version: {api_version}. Use 1 or 2.")

atlassian/confluence_v2.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
Module for Confluence API v2 implementation
6+
"""
7+
8+
import logging
9+
10+
from typing import Dict, List, Optional, Union, Any
11+
12+
from .confluence_base import ConfluenceBase
13+
14+
log = logging.getLogger(__name__)
15+
16+
17+
class ConfluenceV2(ConfluenceBase):
18+
"""
19+
Confluence API v2 implementation class
20+
"""
21+
22+
def __init__(self, url: str, *args, **kwargs):
23+
"""
24+
Initialize the ConfluenceV2 instance with API version 2
25+
26+
Args:
27+
url: Confluence Cloud base URL
28+
*args: Variable length argument list passed to ConfluenceBase
29+
**kwargs: Keyword arguments passed to ConfluenceBase
30+
"""
31+
# Set API version to 2
32+
kwargs.setdefault('api_version', 2)
33+
super(ConfluenceV2, self).__init__(url, *args, **kwargs)
34+
35+
# V2-specific methods will be implemented here in Phase 2 and Phase 3

confluence_v2_implementation_checklist.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,30 @@
1010
<!-- Add any additional information, context, or rules here -->
1111

1212
## Implementation Progress Tracking
13-
- [ ] Phase 1: Core Structure (0% complete)
13+
- [x] Phase 1: Core Structure (80% complete)
1414
- [ ] Phase 2: Core Methods (0% complete)
1515
- [ ] Phase 3: New V2 Features (0% complete)
16-
- [ ] Phase 4: Testing (0% complete)
16+
- [ ] Phase 4: Testing (10% complete)
1717
- [ ] Phase 5: Documentation (0% complete)
1818

1919
## Phase 1: Core Structure
2020

2121
### Version-Aware Base Class
22-
- [ ] Create/modify `ConfluenceBase` class that extends `AtlassianRestAPI`
23-
- [ ] Add API version parameter to constructor (default to v1)
24-
- [ ] Ensure proper URL handling for cloud instances
22+
- [x] Create/modify `ConfluenceBase` class that extends `AtlassianRestAPI`
23+
- [x] Add API version parameter to constructor (default to v1)
24+
- [x] Ensure proper URL handling for cloud instances
2525

2626
### Endpoint Mapping
27-
- [ ] Create `ConfluenceEndpoints` class with V1 and V2 endpoint dictionaries
28-
- [ ] Implement endpoint mapping for all core operations
29-
- [ ] Add method to retrieve appropriate endpoint based on version
27+
- [x] Create `ConfluenceEndpoints` class with V1 and V2 endpoint dictionaries
28+
- [x] Implement endpoint mapping for all core operations
29+
- [x] Add method to retrieve appropriate endpoint based on version
3030

3131
### Version-Aware Pagination
32-
- [ ] Update `_get_paged` method to support both pagination methods
33-
- [ ] Implement cursor-based pagination for V2 API
34-
- [ ] Implement offset-based pagination for V1 API (maintain existing)
35-
- [ ] Handle Link header parsing for V2 API responses
36-
- [ ] Support _links.next property for pagination
32+
- [x] Update `_get_paged` method to support both pagination methods
33+
- [x] Implement cursor-based pagination for V2 API
34+
- [x] Implement offset-based pagination for V1 API (maintain existing)
35+
- [x] Handle Link header parsing for V2 API responses
36+
- [x] Support _links.next property for pagination
3737

3838
## Phase 2: Core Methods
3939

@@ -66,8 +66,8 @@
6666
- [ ] Add deprecation warnings for methods that have renamed equivalents
6767

6868
### Factory Method
69-
- [ ] Implement `factory` static method for easy client creation
70-
- [ ] Support specifying API version in factory method
69+
- [x] Implement `factory` static method for easy client creation
70+
- [x] Support specifying API version in factory method
7171

7272
## Phase 3: New V2 Features
7373

@@ -92,7 +92,7 @@
9292
## Phase 4: Testing
9393

9494
### Test Infrastructure
95-
- [ ] Create test fixtures for both v1 and v2 API
95+
- [x] Create test fixtures for both v1 and v2 API
9696
- [ ] Implement mock responses for all endpoints
9797
- [ ] Add version-specific test classes
9898

0 commit comments

Comments
 (0)