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.
Files changed (65) hide show
  1. docspan/__init__.py +3 -0
  2. docspan/__main__.py +0 -0
  3. docspan/backends/__init__.py +19 -0
  4. docspan/backends/base.py +85 -0
  5. docspan/backends/confluence/__init__.py +0 -0
  6. docspan/backends/confluence/adf/__init__.py +14 -0
  7. docspan/backends/confluence/adf/comparator.py +427 -0
  8. docspan/backends/confluence/adf/converter.py +119 -0
  9. docspan/backends/confluence/adf/converters.py +1449 -0
  10. docspan/backends/confluence/adf/interfaces.py +191 -0
  11. docspan/backends/confluence/adf/nodes.py +2085 -0
  12. docspan/backends/confluence/adf/parser.py +400 -0
  13. docspan/backends/confluence/adf/validators.py +161 -0
  14. docspan/backends/confluence/adf/visitors.py +495 -0
  15. docspan/backends/confluence/backend.py +227 -0
  16. docspan/backends/confluence/client.py +44 -0
  17. docspan/backends/confluence/config/__init__.py +21 -0
  18. docspan/backends/confluence/config/loader.py +107 -0
  19. docspan/backends/confluence/config/models.py +167 -0
  20. docspan/backends/confluence/config/validation.py +297 -0
  21. docspan/backends/confluence/markdown/__init__.py +22 -0
  22. docspan/backends/confluence/markdown/ast.py +819 -0
  23. docspan/backends/confluence/markdown/extensions/__init__.py +5 -0
  24. docspan/backends/confluence/markdown/extensions/frontmatter.py +80 -0
  25. docspan/backends/confluence/markdown/extensions/mermaid.py +64 -0
  26. docspan/backends/confluence/markdown/extensions/wikilinks.py +179 -0
  27. docspan/backends/confluence/markdown/inline_parser.py +495 -0
  28. docspan/backends/confluence/markdown/parser.py +1006 -0
  29. docspan/backends/confluence/models/__init__.py +18 -0
  30. docspan/backends/confluence/models/markdown_file.py +402 -0
  31. docspan/backends/confluence/models/page.py +212 -0
  32. docspan/backends/confluence/models/path_utils.py +34 -0
  33. docspan/backends/confluence/models/results.py +28 -0
  34. docspan/backends/confluence/models/sync_status.py +382 -0
  35. docspan/backends/confluence/services/__init__.py +0 -0
  36. docspan/backends/confluence/services/confluence/__init__.py +40 -0
  37. docspan/backends/confluence/services/confluence/attachment_client.py +147 -0
  38. docspan/backends/confluence/services/confluence/base_client.py +420 -0
  39. docspan/backends/confluence/services/confluence/client.py +376 -0
  40. docspan/backends/confluence/services/confluence/comment_client.py +682 -0
  41. docspan/backends/confluence/services/confluence/crawler.py +587 -0
  42. docspan/backends/confluence/services/confluence/label_client.py +130 -0
  43. docspan/backends/confluence/services/confluence/page_client.py +1288 -0
  44. docspan/backends/confluence/services/confluence/space_client.py +179 -0
  45. docspan/backends/confluence/services/confluence/url_parser.py +106 -0
  46. docspan/backends/google_docs/__init__.py +0 -0
  47. docspan/backends/google_docs/auth.py +143 -0
  48. docspan/backends/google_docs/backend.py +140 -0
  49. docspan/backends/google_docs/client.py +665 -0
  50. docspan/backends/google_docs/converter.py +471 -0
  51. docspan/backends/google_docs/docs_request_builder.py +232 -0
  52. docspan/backends/google_docs/docs_structure_parser.py +120 -0
  53. docspan/backends/google_docs/markdown_to_paragraph_parser.py +145 -0
  54. docspan/cli/__init__.py +0 -0
  55. docspan/cli/main.py +408 -0
  56. docspan/config.py +62 -0
  57. docspan/core/__init__.py +49 -0
  58. docspan/core/merge.py +30 -0
  59. docspan/core/orchestrator.py +332 -0
  60. docspan/core/paths.py +8 -0
  61. docspan/core/state.py +53 -0
  62. docspan-0.1.0.dist-info/METADATA +273 -0
  63. docspan-0.1.0.dist-info/RECORD +65 -0
  64. docspan-0.1.0.dist-info/WHEEL +4 -0
  65. 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
+ }