markdown-to-confluence 0.1.13__py3-none-any.whl → 0.2.1__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.
md2conf/application.py CHANGED
@@ -1,154 +1,231 @@
1
- import logging
2
- import os.path
3
- from pathlib import Path
4
- from typing import Dict, Optional
5
-
6
- from .api import ConfluenceSession
7
- from .converter import (
8
- ConfluenceDocument,
9
- ConfluenceDocumentOptions,
10
- ConfluencePageMetadata,
11
- attachment_name,
12
- extract_qualified_id,
13
- )
14
-
15
- LOGGER = logging.getLogger(__name__)
16
-
17
-
18
- class Application:
19
- "The entry point for Markdown to Confluence conversion."
20
-
21
- api: ConfluenceSession
22
- options: ConfluenceDocumentOptions
23
-
24
- def __init__(
25
- self, api: ConfluenceSession, options: ConfluenceDocumentOptions
26
- ) -> None:
27
- self.api = api
28
- self.options = options
29
-
30
- def synchronize(self, path: Path) -> None:
31
- "Synchronizes a single Markdown page or a directory of Markdown pages."
32
-
33
- if path.is_dir():
34
- self.synchronize_directory(path)
35
- elif path.is_file():
36
- self.synchronize_page(path)
37
- else:
38
- raise ValueError(f"expected: valid file or directory path; got: {path}")
39
-
40
- def synchronize_page(self, page_path: Path) -> None:
41
- "Synchronizes a single Markdown page with Confluence."
42
-
43
- self._synchronize_page(page_path, {})
44
-
45
- def synchronize_directory(self, local_dir: Path) -> None:
46
- "Synchronizes a directory of Markdown pages with Confluence."
47
-
48
- page_metadata: Dict[Path, ConfluencePageMetadata] = {}
49
- LOGGER.info(f"Synchronizing directory: {local_dir}")
50
-
51
- # Step 1: build index of all page metadata
52
- # NOTE: Pathlib.walk() is implemented only in Python 3.12+
53
- # so sticking for old os.walk
54
- for root, directories, files in os.walk(local_dir):
55
- for file_name in files:
56
- # Reconstitute Path object back
57
- docfile = (Path(root) / file_name).absolute()
58
-
59
- # Skip non-markdown files
60
- if docfile.suffix.lower() != ".md":
61
- continue
62
- metadata = self._get_or_create_page(docfile)
63
-
64
- LOGGER.debug(f"indexed {docfile} with metadata: {metadata}")
65
- page_metadata[docfile] = metadata
66
-
67
- LOGGER.info(f"indexed {len(page_metadata)} pages")
68
-
69
- # Step 2: Convert each page
70
- for page_path in page_metadata.keys():
71
- self._synchronize_page(page_path, page_metadata)
72
-
73
- def _synchronize_page(
74
- self,
75
- page_path: Path,
76
- page_metadata: Dict[Path, ConfluencePageMetadata],
77
- ) -> None:
78
- base_path = page_path.parent
79
-
80
- LOGGER.info(f"Synchronizing page: {page_path}")
81
- document = ConfluenceDocument(page_path, self.options, page_metadata)
82
-
83
- if document.id.space_key:
84
- with self.api.switch_space(document.id.space_key):
85
- self._update_document(document, base_path)
86
- else:
87
- self._update_document(document, base_path)
88
-
89
- def _get_or_create_page(
90
- self, absolute_path: Path, title: Optional[str] = None
91
- ) -> ConfluencePageMetadata:
92
- """
93
- Creates a new Confluence page if no page is linked in the Markdown document.
94
- """
95
-
96
- # parse file
97
- with open(absolute_path, "r", encoding="utf-8") as f:
98
- document = f.read()
99
-
100
- qualified_id, document = extract_qualified_id(document)
101
- if qualified_id is not None:
102
- confluence_page = self.api.get_page(
103
- qualified_id.page_id, space_key=qualified_id.space_key
104
- )
105
- else:
106
- if self.options.root_page_id is None:
107
- raise ValueError(
108
- "expected: Confluence page ID to act as parent for Markdown files with no linked Confluence page"
109
- )
110
-
111
- # use file name without extension if no title is supplied
112
- if title is None:
113
- title = absolute_path.stem
114
-
115
- confluence_page = self.api.get_or_create_page(
116
- title, self.options.root_page_id
117
- )
118
- self._update_markdown(
119
- absolute_path,
120
- document,
121
- confluence_page.id,
122
- confluence_page.space_key,
123
- )
124
-
125
- return ConfluencePageMetadata(
126
- domain=self.api.domain,
127
- base_path=self.api.base_path,
128
- page_id=confluence_page.id,
129
- space_key=confluence_page.space_key or self.api.space_key,
130
- title=confluence_page.title or "",
131
- )
132
-
133
- def _update_document(self, document: ConfluenceDocument, base_path: Path) -> None:
134
- for image in document.images:
135
- self.api.upload_attachment(
136
- document.id.page_id, base_path / image, attachment_name(image), ""
137
- )
138
-
139
- content = document.xhtml()
140
- LOGGER.debug(f"generated Confluence Storage Format document:\n{content}")
141
- self.api.update_page(document.id.page_id, content)
142
-
143
- def _update_markdown(
144
- self,
145
- path: Path,
146
- document: str,
147
- page_id: str,
148
- space_key: Optional[str],
149
- ) -> None:
150
- with open(path, "w", encoding="utf-8") as file:
151
- file.write(f"<!-- confluence-page-id: {page_id} -->\n")
152
- if space_key:
153
- file.write(f"<!-- confluence-space-key: {space_key} -->\n")
154
- file.write(document)
1
+ import logging
2
+ import os.path
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional
5
+
6
+ from .api import ConfluencePage, ConfluenceSession
7
+ from .converter import (
8
+ ConfluenceDocument,
9
+ ConfluenceDocumentOptions,
10
+ ConfluencePageMetadata,
11
+ ConfluenceQualifiedID,
12
+ attachment_name,
13
+ extract_qualified_id,
14
+ read_qualified_id,
15
+ )
16
+
17
+ LOGGER = logging.getLogger(__name__)
18
+
19
+
20
+ class Application:
21
+ "The entry point for Markdown to Confluence conversion."
22
+
23
+ api: ConfluenceSession
24
+ options: ConfluenceDocumentOptions
25
+
26
+ def __init__(
27
+ self, api: ConfluenceSession, options: ConfluenceDocumentOptions
28
+ ) -> None:
29
+ self.api = api
30
+ self.options = options
31
+
32
+ def synchronize(self, path: Path) -> None:
33
+ "Synchronizes a single Markdown page or a directory of Markdown pages."
34
+
35
+ if path.is_dir():
36
+ self.synchronize_directory(path)
37
+ elif path.is_file():
38
+ self.synchronize_page(path)
39
+ else:
40
+ raise ValueError(f"expected: valid file or directory path; got: {path}")
41
+
42
+ def synchronize_page(self, page_path: Path) -> None:
43
+ "Synchronizes a single Markdown page with Confluence."
44
+
45
+ self._synchronize_page(page_path, {})
46
+
47
+ def synchronize_directory(self, local_dir: Path) -> None:
48
+ "Synchronizes a directory of Markdown pages with Confluence."
49
+
50
+ LOGGER.info(f"Synchronizing directory: {local_dir}")
51
+
52
+ # Step 1: build index of all page metadata
53
+ page_metadata: Dict[Path, ConfluencePageMetadata] = {}
54
+ root_id = (
55
+ ConfluenceQualifiedID(self.options.root_page_id, self.api.space_key)
56
+ if self.options.root_page_id
57
+ else None
58
+ )
59
+ self._index_directory(local_dir, root_id, page_metadata)
60
+ LOGGER.info(f"indexed {len(page_metadata)} page(s)")
61
+
62
+ # Step 2: convert each page
63
+ for page_path in page_metadata.keys():
64
+ self._synchronize_page(page_path, page_metadata)
65
+
66
+ def _synchronize_page(
67
+ self,
68
+ page_path: Path,
69
+ page_metadata: Dict[Path, ConfluencePageMetadata],
70
+ ) -> None:
71
+ base_path = page_path.parent
72
+
73
+ LOGGER.info(f"Synchronizing page: {page_path}")
74
+ document = ConfluenceDocument(page_path, self.options, page_metadata)
75
+
76
+ if document.id.space_key:
77
+ with self.api.switch_space(document.id.space_key):
78
+ self._update_document(document, base_path)
79
+ else:
80
+ self._update_document(document, base_path)
81
+
82
+ def _index_directory(
83
+ self,
84
+ local_dir: Path,
85
+ root_id: Optional[ConfluenceQualifiedID],
86
+ page_metadata: Dict[Path, ConfluencePageMetadata],
87
+ ) -> None:
88
+ "Indexes Markdown files in a directory recursively."
89
+
90
+ LOGGER.info(f"Indexing directory: {local_dir}")
91
+
92
+ files: List[Path] = []
93
+ directories: List[Path] = []
94
+ for entry in os.scandir(local_dir):
95
+ if entry.is_file():
96
+ if entry.name.endswith(".md"):
97
+ # skip non-markdown files
98
+ files.append((Path(local_dir) / entry.name).absolute())
99
+ elif entry.is_dir():
100
+ if not entry.name.startswith("."):
101
+ directories.append((Path(local_dir) / entry.name).absolute())
102
+
103
+ # make page act as parent node in Confluence
104
+ parent_id: Optional[ConfluenceQualifiedID] = None
105
+ if "index.md" in files:
106
+ parent_id = read_qualified_id(Path(local_dir) / "index.md")
107
+ elif "README.md" in files:
108
+ parent_id = read_qualified_id(Path(local_dir) / "README.md")
109
+
110
+ if parent_id is None:
111
+ parent_id = root_id
112
+
113
+ for doc in files:
114
+ metadata = self._get_or_create_page(doc, parent_id)
115
+ LOGGER.debug(f"indexed {doc} with metadata: {metadata}")
116
+ page_metadata[doc] = metadata
117
+
118
+ for directory in directories:
119
+ self._index_directory(Path(local_dir) / directory, parent_id, page_metadata)
120
+
121
+ def _get_or_create_page(
122
+ self,
123
+ absolute_path: Path,
124
+ parent_id: Optional[ConfluenceQualifiedID],
125
+ *,
126
+ title: Optional[str] = None,
127
+ ) -> ConfluencePageMetadata:
128
+ """
129
+ Creates a new Confluence page if no page is linked in the Markdown document.
130
+ """
131
+
132
+ # parse file
133
+ with open(absolute_path, "r", encoding="utf-8") as f:
134
+ document = f.read()
135
+
136
+ qualified_id, document = extract_qualified_id(document)
137
+ if qualified_id is not None:
138
+ confluence_page = self.api.get_page(
139
+ qualified_id.page_id, space_key=qualified_id.space_key
140
+ )
141
+ else:
142
+ if parent_id is None:
143
+ raise ValueError(
144
+ "expected: Confluence page ID to act as parent for Markdown files with no linked Confluence page"
145
+ )
146
+
147
+ confluence_page = self._create_page(
148
+ absolute_path, document, title, parent_id
149
+ )
150
+
151
+ return ConfluencePageMetadata(
152
+ domain=self.api.domain,
153
+ base_path=self.api.base_path,
154
+ page_id=confluence_page.id,
155
+ space_key=confluence_page.space_key or self.api.space_key,
156
+ title=confluence_page.title or "",
157
+ )
158
+
159
+ def _create_page(
160
+ self,
161
+ absolute_path: Path,
162
+ document: str,
163
+ title: Optional[str],
164
+ parent_id: ConfluenceQualifiedID,
165
+ ) -> ConfluencePage:
166
+ "Creates a new Confluence page when Markdown file doesn't have an embedded page ID yet."
167
+
168
+ # use file name without extension if no title is supplied
169
+ if title is None:
170
+ title = absolute_path.stem
171
+
172
+ confluence_page = self.api.get_or_create_page(
173
+ title, parent_id.page_id, space_key=parent_id.space_key
174
+ )
175
+ self._update_markdown(
176
+ absolute_path,
177
+ document,
178
+ confluence_page.id,
179
+ confluence_page.space_key,
180
+ )
181
+ return confluence_page
182
+
183
+ def _update_document(self, document: ConfluenceDocument, base_path: Path) -> None:
184
+ "Saves a new version of a Confluence document."
185
+
186
+ for image in document.images:
187
+ self.api.upload_attachment(
188
+ document.id.page_id,
189
+ base_path / image,
190
+ attachment_name(image),
191
+ )
192
+
193
+ for image, data in document.embedded_images.items():
194
+ self.api.upload_attachment(
195
+ document.id.page_id,
196
+ Path("EMB") / image,
197
+ attachment_name(image),
198
+ raw_data=data,
199
+ )
200
+
201
+ content = document.xhtml()
202
+ LOGGER.debug(f"generated Confluence Storage Format document:\n{content}")
203
+ self.api.update_page(document.id.page_id, content)
204
+
205
+ def _update_markdown(
206
+ self,
207
+ path: Path,
208
+ document: str,
209
+ page_id: str,
210
+ space_key: Optional[str],
211
+ ) -> None:
212
+ "Writes the Confluence page ID and space key at the beginning of the Markdown file."
213
+
214
+ content: List[str] = []
215
+
216
+ # check if the file has frontmatter
217
+ index = 0
218
+ if document.startswith("---\n"):
219
+ index = document.find("\n---\n", 4) + 4
220
+
221
+ # insert the Confluence keys after the frontmatter
222
+ content.append(document[:index])
223
+
224
+ content.append(f"<!-- confluence-page-id: {page_id} -->")
225
+ if space_key:
226
+ content.append(f"<!-- confluence-space-key: {space_key} -->")
227
+
228
+ content.append(document[index:])
229
+
230
+ with open(path, "w", encoding="utf-8") as file:
231
+ file.write("\n".join(content))