docspan 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docspan/__init__.py +3 -0
- docspan/__main__.py +0 -0
- docspan/backends/__init__.py +19 -0
- docspan/backends/base.py +85 -0
- docspan/backends/confluence/__init__.py +0 -0
- docspan/backends/confluence/adf/__init__.py +14 -0
- docspan/backends/confluence/adf/comparator.py +427 -0
- docspan/backends/confluence/adf/converter.py +119 -0
- docspan/backends/confluence/adf/converters.py +1449 -0
- docspan/backends/confluence/adf/interfaces.py +191 -0
- docspan/backends/confluence/adf/nodes.py +2085 -0
- docspan/backends/confluence/adf/parser.py +400 -0
- docspan/backends/confluence/adf/validators.py +161 -0
- docspan/backends/confluence/adf/visitors.py +495 -0
- docspan/backends/confluence/backend.py +227 -0
- docspan/backends/confluence/client.py +44 -0
- docspan/backends/confluence/config/__init__.py +21 -0
- docspan/backends/confluence/config/loader.py +107 -0
- docspan/backends/confluence/config/models.py +167 -0
- docspan/backends/confluence/config/validation.py +297 -0
- docspan/backends/confluence/markdown/__init__.py +22 -0
- docspan/backends/confluence/markdown/ast.py +819 -0
- docspan/backends/confluence/markdown/extensions/__init__.py +5 -0
- docspan/backends/confluence/markdown/extensions/frontmatter.py +80 -0
- docspan/backends/confluence/markdown/extensions/mermaid.py +64 -0
- docspan/backends/confluence/markdown/extensions/wikilinks.py +179 -0
- docspan/backends/confluence/markdown/inline_parser.py +495 -0
- docspan/backends/confluence/markdown/parser.py +1006 -0
- docspan/backends/confluence/models/__init__.py +18 -0
- docspan/backends/confluence/models/markdown_file.py +402 -0
- docspan/backends/confluence/models/page.py +212 -0
- docspan/backends/confluence/models/path_utils.py +34 -0
- docspan/backends/confluence/models/results.py +28 -0
- docspan/backends/confluence/models/sync_status.py +382 -0
- docspan/backends/confluence/services/__init__.py +0 -0
- docspan/backends/confluence/services/confluence/__init__.py +40 -0
- docspan/backends/confluence/services/confluence/attachment_client.py +147 -0
- docspan/backends/confluence/services/confluence/base_client.py +420 -0
- docspan/backends/confluence/services/confluence/client.py +376 -0
- docspan/backends/confluence/services/confluence/comment_client.py +682 -0
- docspan/backends/confluence/services/confluence/crawler.py +587 -0
- docspan/backends/confluence/services/confluence/label_client.py +130 -0
- docspan/backends/confluence/services/confluence/page_client.py +1288 -0
- docspan/backends/confluence/services/confluence/space_client.py +179 -0
- docspan/backends/confluence/services/confluence/url_parser.py +106 -0
- docspan/backends/google_docs/__init__.py +0 -0
- docspan/backends/google_docs/auth.py +143 -0
- docspan/backends/google_docs/backend.py +140 -0
- docspan/backends/google_docs/client.py +665 -0
- docspan/backends/google_docs/converter.py +471 -0
- docspan/backends/google_docs/docs_request_builder.py +232 -0
- docspan/backends/google_docs/docs_structure_parser.py +120 -0
- docspan/backends/google_docs/markdown_to_paragraph_parser.py +145 -0
- docspan/cli/__init__.py +0 -0
- docspan/cli/main.py +408 -0
- docspan/config.py +62 -0
- docspan/core/__init__.py +49 -0
- docspan/core/merge.py +30 -0
- docspan/core/orchestrator.py +332 -0
- docspan/core/paths.py +8 -0
- docspan/core/state.py +53 -0
- docspan-0.1.0.dist-info/METADATA +273 -0
- docspan-0.1.0.dist-info/RECORD +65 -0
- docspan-0.1.0.dist-info/WHEEL +4 -0
- docspan-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Facade client for interacting with Confluence API.
|
|
3
|
+
|
|
4
|
+
This module provides a facade over specialized Confluence clients,
|
|
5
|
+
maintaining backward compatibility with the original monolithic client.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from docspan.backends.confluence.config.models import ConfluenceConfig
|
|
15
|
+
from docspan.backends.confluence.models.page import ConfluencePage
|
|
16
|
+
from docspan.backends.confluence.services.confluence.attachment_client import AttachmentClient
|
|
17
|
+
from docspan.backends.confluence.services.confluence.base_client import (
|
|
18
|
+
ArchivedPageError,
|
|
19
|
+
ConfluenceApiError,
|
|
20
|
+
PageNotFoundError,
|
|
21
|
+
)
|
|
22
|
+
from docspan.backends.confluence.services.confluence.comment_client import ConfluenceCommentClient
|
|
23
|
+
from docspan.backends.confluence.services.confluence.label_client import LabelClient
|
|
24
|
+
from docspan.backends.confluence.services.confluence.page_client import PageClient
|
|
25
|
+
from docspan.backends.confluence.services.confluence.space_client import SpaceClient
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConfluenceClient:
|
|
29
|
+
"""
|
|
30
|
+
Facade client for interacting with Confluence REST API.
|
|
31
|
+
|
|
32
|
+
This class delegates to specialized client implementations while
|
|
33
|
+
maintaining backward compatibility with the original client.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
config: Confluence configuration
|
|
37
|
+
page_client: Client for page operations
|
|
38
|
+
attachment_client: Client for attachment operations
|
|
39
|
+
label_client: Client for label operations
|
|
40
|
+
space_client: Client for space operations
|
|
41
|
+
base_url: Base URL for the Confluence instance
|
|
42
|
+
rest_api_url: URL for the REST API
|
|
43
|
+
logger: Logger instance
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, config: ConfluenceConfig):
|
|
47
|
+
"""
|
|
48
|
+
Initialize the Confluence client facade.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
config: Confluence configuration
|
|
52
|
+
"""
|
|
53
|
+
self.config = config
|
|
54
|
+
|
|
55
|
+
# Initialize specialized clients
|
|
56
|
+
self.page_client = PageClient(config)
|
|
57
|
+
self.attachment_client = AttachmentClient(config)
|
|
58
|
+
self.comment_client = ConfluenceCommentClient(config)
|
|
59
|
+
self.label_client = LabelClient(config)
|
|
60
|
+
self.space_client = SpaceClient(config)
|
|
61
|
+
|
|
62
|
+
# Set up common attributes for backward compatibility
|
|
63
|
+
self.session = self.page_client.session
|
|
64
|
+
self.base_url = config.base_url.rstrip("/")
|
|
65
|
+
|
|
66
|
+
if "/wiki" not in self.base_url:
|
|
67
|
+
self.rest_api_url = f"{self.base_url}/wiki/rest/api"
|
|
68
|
+
else:
|
|
69
|
+
self.rest_api_url = f"{self.base_url}/rest/api"
|
|
70
|
+
|
|
71
|
+
self.logger = logging.getLogger(__name__)
|
|
72
|
+
|
|
73
|
+
# Cache for parent page information
|
|
74
|
+
self._parent_page_info = None
|
|
75
|
+
|
|
76
|
+
# Page operations
|
|
77
|
+
|
|
78
|
+
def get_page(self, page_id: str) -> Dict[str, Any]:
|
|
79
|
+
"""
|
|
80
|
+
Get a page from Confluence by ID.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
page_id: Confluence page ID
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Page data
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
requests.RequestException: If the API call fails
|
|
90
|
+
ValueError: If the page does not exist
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
return self.page_client.get_page(page_id)
|
|
94
|
+
except PageNotFoundError as e:
|
|
95
|
+
# Convert to requests.HTTPError for backward compatibility
|
|
96
|
+
raise requests.HTTPError(str(e), response=e.response)
|
|
97
|
+
except ConfluenceApiError as e:
|
|
98
|
+
# Convert to requests.HTTPError for backward compatibility
|
|
99
|
+
if e.response:
|
|
100
|
+
raise requests.HTTPError(str(e), response=e.response)
|
|
101
|
+
else:
|
|
102
|
+
raise requests.RequestException(str(e))
|
|
103
|
+
|
|
104
|
+
def find_page_by_title(self, space_key: str, title: str) -> Optional[Dict[str, Any]]:
|
|
105
|
+
"""
|
|
106
|
+
Find a page by title within a space.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
space_key: Confluence space key
|
|
110
|
+
title: Page title to find
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Page data or None if not found
|
|
114
|
+
"""
|
|
115
|
+
return self.page_client.find_page_by_title(space_key, title)
|
|
116
|
+
|
|
117
|
+
def create_page(self, page: ConfluencePage) -> Dict[str, Any]:
|
|
118
|
+
"""
|
|
119
|
+
Create a new page in Confluence.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
page: Page data to create
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Created page data
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
result = self.page_client.create_page(page)
|
|
129
|
+
|
|
130
|
+
# Add labels if specified (maintain backward compatibility)
|
|
131
|
+
if page.labels and result.get("id"):
|
|
132
|
+
self.add_labels(result["id"], page.labels)
|
|
133
|
+
|
|
134
|
+
return result
|
|
135
|
+
except ConfluenceApiError as e:
|
|
136
|
+
# Convert to requests.HTTPError for backward compatibility
|
|
137
|
+
if e.response:
|
|
138
|
+
raise requests.HTTPError(str(e), response=e.response)
|
|
139
|
+
else:
|
|
140
|
+
raise requests.RequestException(str(e))
|
|
141
|
+
|
|
142
|
+
def update_page(self, page: ConfluencePage) -> Dict[str, Any]:
|
|
143
|
+
"""
|
|
144
|
+
Update an existing page in Confluence.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
page: Page data to update
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Updated page data or error information
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
result = self.page_client.update_page(page)
|
|
154
|
+
|
|
155
|
+
# Add labels if specified (maintain backward compatibility)
|
|
156
|
+
if page.labels and isinstance(result, dict) and result.get("id"):
|
|
157
|
+
self.add_labels(result["id"], page.labels)
|
|
158
|
+
|
|
159
|
+
return result
|
|
160
|
+
except ArchivedPageError as e:
|
|
161
|
+
# Return structured error for archived pages (backward compatibility)
|
|
162
|
+
return self._create_archived_page_error(page, str(e))
|
|
163
|
+
except PageNotFoundError as e:
|
|
164
|
+
# Return structured error for not found pages (backward compatibility)
|
|
165
|
+
return {
|
|
166
|
+
"status": "error",
|
|
167
|
+
"error_type": "page_not_found",
|
|
168
|
+
"message": f"Page {page.id} not found",
|
|
169
|
+
"page_id": page.id,
|
|
170
|
+
"title": page.title,
|
|
171
|
+
"original_error": str(e)
|
|
172
|
+
}
|
|
173
|
+
except ConfluenceApiError as e:
|
|
174
|
+
# Convert to requests.HTTPError for backward compatibility
|
|
175
|
+
if e.response:
|
|
176
|
+
raise requests.HTTPError(str(e), response=e.response)
|
|
177
|
+
else:
|
|
178
|
+
raise requests.RequestException(str(e))
|
|
179
|
+
|
|
180
|
+
def unarchive_page(self, page_id: str) -> Dict[str, Any]:
|
|
181
|
+
"""
|
|
182
|
+
Unarchive a page in Confluence.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
page_id: Confluence page ID to unarchive
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
API response
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
requests.RequestException: If the unarchive operation fails
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
return self.page_client.unarchive_page(page_id)
|
|
195
|
+
except ConfluenceApiError as e:
|
|
196
|
+
# Convert to requests.HTTPError for backward compatibility
|
|
197
|
+
if e.response:
|
|
198
|
+
raise requests.HTTPError(str(e), response=e.response)
|
|
199
|
+
else:
|
|
200
|
+
raise requests.RequestException(str(e))
|
|
201
|
+
|
|
202
|
+
def delete_page(self, page_id: str, trash: bool = True, current_status: str = "current") -> Dict[str, Any]:
|
|
203
|
+
"""
|
|
204
|
+
Delete a page from Confluence.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
page_id: Confluence page ID to delete
|
|
208
|
+
trash: Whether to move the page to trash or permanently delete it
|
|
209
|
+
current_status: The current status of the page ("current", "archived", "draft", etc.)
|
|
210
|
+
This is important for the Confluence API to properly handle deletion
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
API response
|
|
214
|
+
|
|
215
|
+
Notes:
|
|
216
|
+
Confluence DELETE API requires different handling based on page status:
|
|
217
|
+
- If page is "current", it will be trashed
|
|
218
|
+
- If page is "archived" or "trashed", it needs different treatment
|
|
219
|
+
"""
|
|
220
|
+
try:
|
|
221
|
+
# If the page is archived, try to unarchive it first
|
|
222
|
+
if current_status == "archived":
|
|
223
|
+
try:
|
|
224
|
+
self.logger.info(f"Page {page_id} is archived. Attempting to unarchive first...")
|
|
225
|
+
self.unarchive_page(page_id)
|
|
226
|
+
current_status = "current" # Update status after unarchiving
|
|
227
|
+
self.logger.info(f"Page {page_id} successfully unarchived. Proceeding with deletion...")
|
|
228
|
+
except Exception as e:
|
|
229
|
+
self.logger.warning(f"Failed to unarchive page {page_id}: {e}. Proceeding with direct deletion...")
|
|
230
|
+
|
|
231
|
+
return self.page_client.delete_page(page_id, trash, current_status)
|
|
232
|
+
except ConfluenceApiError as e:
|
|
233
|
+
# Convert to requests.HTTPError for backward compatibility
|
|
234
|
+
if e.response:
|
|
235
|
+
raise requests.HTTPError(str(e), response=e.response)
|
|
236
|
+
else:
|
|
237
|
+
raise requests.RequestException(str(e))
|
|
238
|
+
|
|
239
|
+
# Attachment operations
|
|
240
|
+
|
|
241
|
+
def upload_attachment(
|
|
242
|
+
self, page_id: str, file_path: Path, comment: Optional[str] = None
|
|
243
|
+
) -> Dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Upload an attachment to a Confluence page.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
page_id: Confluence page ID
|
|
249
|
+
file_path: Path to the file to upload
|
|
250
|
+
comment: Optional comment for the attachment
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Attachment data
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
return self.attachment_client.upload_attachment(page_id, file_path, comment)
|
|
257
|
+
except ConfluenceApiError as e:
|
|
258
|
+
# Convert to requests.HTTPError for backward compatibility
|
|
259
|
+
if e.response:
|
|
260
|
+
raise requests.HTTPError(str(e), response=e.response)
|
|
261
|
+
else:
|
|
262
|
+
raise requests.RequestException(str(e))
|
|
263
|
+
|
|
264
|
+
# Label operations
|
|
265
|
+
|
|
266
|
+
def add_label(self, page_id: str, label: str) -> Dict[str, Any]:
|
|
267
|
+
"""
|
|
268
|
+
Add a label to a page.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
page_id: Page ID
|
|
272
|
+
label: Label to add
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
API response
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
return self.label_client.add_label(page_id, label)
|
|
279
|
+
except ConfluenceApiError as e:
|
|
280
|
+
# Convert to requests.HTTPError for backward compatibility
|
|
281
|
+
if e.response:
|
|
282
|
+
raise requests.HTTPError(str(e), response=e.response)
|
|
283
|
+
else:
|
|
284
|
+
raise requests.RequestException(str(e))
|
|
285
|
+
|
|
286
|
+
def add_labels(self, page_id: str, labels: List[str]) -> None:
|
|
287
|
+
"""
|
|
288
|
+
Add multiple labels to a page.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
page_id: Page ID
|
|
292
|
+
labels: Labels to add
|
|
293
|
+
"""
|
|
294
|
+
for label in labels:
|
|
295
|
+
self.add_label(page_id, label)
|
|
296
|
+
|
|
297
|
+
# Space key lookup
|
|
298
|
+
|
|
299
|
+
def get_space_key_from_parent(self, parent_id: str) -> Optional[str]:
|
|
300
|
+
"""
|
|
301
|
+
Retrieve the space key from a parent page ID.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
parent_id: Parent page ID
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Space key if found, None otherwise
|
|
308
|
+
"""
|
|
309
|
+
return self.page_client.get_space_key_from_parent(parent_id)
|
|
310
|
+
|
|
311
|
+
# Helper methods for backward compatibility
|
|
312
|
+
|
|
313
|
+
def _extract_error_from_response(self, response) -> Dict[str, Any]:
|
|
314
|
+
"""
|
|
315
|
+
Extract error details from a response.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
response: HTTP response
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Dictionary with error details
|
|
322
|
+
"""
|
|
323
|
+
try:
|
|
324
|
+
return response.json()
|
|
325
|
+
except Exception:
|
|
326
|
+
try:
|
|
327
|
+
return {"message": response.text}
|
|
328
|
+
except Exception:
|
|
329
|
+
self.logger.debug("Could not extract error body from response", exc_info=True)
|
|
330
|
+
return {"message": "Unknown error"}
|
|
331
|
+
|
|
332
|
+
def _is_archived_page_error(self, response, error_message: str) -> bool:
|
|
333
|
+
"""
|
|
334
|
+
Check if an error indicates an archived page.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
response: HTTP response
|
|
338
|
+
error_message: Error message string
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
True if error indicates archived page, False otherwise
|
|
342
|
+
"""
|
|
343
|
+
# Delegate to the page client's implementation
|
|
344
|
+
if isinstance(self.page_client, PageClient) and hasattr(self.page_client, "_is_archived_page_error"):
|
|
345
|
+
# Access protected method for backward compatibility
|
|
346
|
+
return self.page_client._is_archived_page_error(response, error_message)
|
|
347
|
+
|
|
348
|
+
# Fallback implementation
|
|
349
|
+
if "PermissionException" in error_message and any([
|
|
350
|
+
"Could not update Content" in error_message,
|
|
351
|
+
"Parent Content doesn't exist" in error_message,
|
|
352
|
+
"No parent content exists" in error_message
|
|
353
|
+
]):
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
def _create_archived_page_error(self, page: ConfluencePage, error_message: str) -> Dict[str, Any]:
|
|
359
|
+
"""
|
|
360
|
+
Create a standardized error response for archived pages.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
page: Page that was being updated
|
|
364
|
+
error_message: Error message from the API
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Dictionary with error information
|
|
368
|
+
"""
|
|
369
|
+
return {
|
|
370
|
+
"status": "error",
|
|
371
|
+
"error_type": "archived_page",
|
|
372
|
+
"message": "Page appears to be archived or restricted",
|
|
373
|
+
"page_id": page.id,
|
|
374
|
+
"title": page.title,
|
|
375
|
+
"original_error": error_message
|
|
376
|
+
}
|