markdown-to-confluence 0.3.5__py3-none-any.whl → 0.4.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.
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/METADATA +150 -17
- markdown_to_confluence-0.4.1.dist-info/RECORD +25 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +20 -17
- md2conf/api.py +529 -216
- md2conf/application.py +85 -96
- md2conf/collection.py +31 -0
- md2conf/converter.py +99 -78
- md2conf/emoji.py +28 -3
- md2conf/extra.py +27 -0
- md2conf/local.py +28 -41
- md2conf/matcher.py +1 -3
- md2conf/mermaid.py +2 -7
- md2conf/metadata.py +0 -2
- md2conf/processor.py +135 -57
- md2conf/properties.py +66 -14
- md2conf/scanner.py +56 -23
- markdown_to_confluence-0.3.5.dist-info/RECORD +0 -23
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/zip-safe +0 -0
md2conf/application.py
CHANGED
|
@@ -6,22 +6,16 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import hashlib
|
|
10
9
|
import logging
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
from typing import Optional
|
|
13
12
|
|
|
14
|
-
from .api import
|
|
15
|
-
from .converter import
|
|
16
|
-
|
|
17
|
-
ConfluenceDocumentOptions,
|
|
18
|
-
ConfluencePageID,
|
|
19
|
-
attachment_name,
|
|
20
|
-
)
|
|
13
|
+
from .api import ConfluenceContentProperty, ConfluenceLabel, ConfluenceSession, ConfluenceStatus
|
|
14
|
+
from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID, attachment_name
|
|
15
|
+
from .extra import override, path_relative_to
|
|
21
16
|
from .metadata import ConfluencePageMetadata
|
|
22
|
-
from .processor import Converter, Processor, ProcessorFactory
|
|
17
|
+
from .processor import Converter, DocumentNode, Processor, ProcessorFactory
|
|
23
18
|
from .properties import PageError
|
|
24
|
-
from .scanner import Scanner
|
|
25
19
|
|
|
26
20
|
LOGGER = logging.getLogger(__name__)
|
|
27
21
|
|
|
@@ -33,9 +27,7 @@ class SynchronizingProcessor(Processor):
|
|
|
33
27
|
|
|
34
28
|
api: ConfluenceSession
|
|
35
29
|
|
|
36
|
-
def __init__(
|
|
37
|
-
self, api: ConfluenceSession, options: ConfluenceDocumentOptions, root_dir: Path
|
|
38
|
-
) -> None:
|
|
30
|
+
def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions, root_dir: Path) -> None:
|
|
39
31
|
"""
|
|
40
32
|
Initializes a new processor instance.
|
|
41
33
|
|
|
@@ -47,71 +39,72 @@ class SynchronizingProcessor(Processor):
|
|
|
47
39
|
super().__init__(options, api.site, root_dir)
|
|
48
40
|
self.api = api
|
|
49
41
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
) -> ConfluencePageMetadata:
|
|
42
|
+
@override
|
|
43
|
+
def _synchronize_tree(self, root: DocumentNode, root_id: Optional[ConfluencePageID]) -> None:
|
|
53
44
|
"""
|
|
54
|
-
Creates
|
|
45
|
+
Creates the cross-reference index and synchronizes the directory tree structure with the Confluence page hierarchy.
|
|
46
|
+
|
|
47
|
+
Creates new Confluence pages as necessary, e.g. if no page is linked in the Markdown document, or no page is found with lookup by page title.
|
|
48
|
+
|
|
49
|
+
Updates the original Markdown document to add tags to associate the document with its corresponding Confluence page.
|
|
55
50
|
"""
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# use file name (without extension) and path hash if no title is supplied
|
|
69
|
-
if document.title is not None:
|
|
70
|
-
title = document.title
|
|
71
|
-
else:
|
|
72
|
-
overwrite = True
|
|
73
|
-
relative_path = absolute_path.relative_to(self.root_dir)
|
|
74
|
-
hash = hashlib.md5(relative_path.as_posix().encode("utf-8"))
|
|
75
|
-
digest = "".join(f"{c:x}" for c in hash.digest())
|
|
76
|
-
title = f"{absolute_path.stem} [{digest}]"
|
|
77
|
-
|
|
78
|
-
confluence_page = self._create_page(
|
|
79
|
-
absolute_path, document.text, title, parent_id
|
|
80
|
-
)
|
|
52
|
+
if root.page_id is None and root_id is None:
|
|
53
|
+
raise PageError(f"expected: root page ID in options, or explicit page ID in {root.absolute_path}")
|
|
54
|
+
elif root.page_id is not None and root_id is not None:
|
|
55
|
+
if root.page_id != root_id.page_id:
|
|
56
|
+
raise PageError(f"mismatched inferred page ID of {root_id.page_id} and explicit page ID in {root.absolute_path}")
|
|
57
|
+
|
|
58
|
+
real_id = root_id
|
|
59
|
+
elif root_id is not None:
|
|
60
|
+
real_id = root_id
|
|
61
|
+
elif root.page_id is not None:
|
|
62
|
+
real_id = ConfluencePageID(root.page_id)
|
|
81
63
|
else:
|
|
82
|
-
|
|
83
|
-
confluence_page = self.api.get_page(document.page_id)
|
|
84
|
-
|
|
85
|
-
return ConfluencePageMetadata(
|
|
86
|
-
page_id=confluence_page.id,
|
|
87
|
-
space_key=self.api.space_id_to_key(confluence_page.space_id),
|
|
88
|
-
title=confluence_page.title,
|
|
89
|
-
overwrite=overwrite,
|
|
90
|
-
)
|
|
64
|
+
raise NotImplementedError("condition not exhaustive")
|
|
91
65
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
66
|
+
self._synchronize_subtree(root, real_id)
|
|
67
|
+
|
|
68
|
+
def _synchronize_subtree(self, node: DocumentNode, parent_id: ConfluencePageID) -> None:
|
|
69
|
+
if node.page_id is not None:
|
|
70
|
+
# verify if page exists
|
|
71
|
+
page = self.api.get_page_properties(node.page_id)
|
|
72
|
+
update = False
|
|
73
|
+
elif node.title is not None:
|
|
74
|
+
# look up page by title
|
|
75
|
+
page = self.api.get_or_create_page(node.title, parent_id.page_id)
|
|
76
|
+
|
|
77
|
+
if page.status is ConfluenceStatus.ARCHIVED:
|
|
78
|
+
raise PageError(f"unable to update archived page with ID {page.id}")
|
|
102
79
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
self.api.
|
|
80
|
+
update = True
|
|
81
|
+
else:
|
|
82
|
+
# always create a new page
|
|
83
|
+
digest = self._generate_hash(node.absolute_path)
|
|
84
|
+
title = f"{node.absolute_path.stem} [{digest}]"
|
|
85
|
+
page = self.api.create_page(parent_id.page_id, title, "")
|
|
86
|
+
update = True
|
|
87
|
+
|
|
88
|
+
space_key = self.api.space_id_to_key(page.spaceId)
|
|
89
|
+
if update:
|
|
90
|
+
self._update_markdown(
|
|
91
|
+
node.absolute_path,
|
|
92
|
+
page_id=page.id,
|
|
93
|
+
space_key=space_key,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
data = ConfluencePageMetadata(
|
|
97
|
+
page_id=page.id,
|
|
98
|
+
space_key=space_key,
|
|
99
|
+
title=page.title,
|
|
109
100
|
)
|
|
110
|
-
|
|
101
|
+
self.page_metadata.add(node.absolute_path, data)
|
|
102
|
+
|
|
103
|
+
for child_node in node.children():
|
|
104
|
+
self._synchronize_subtree(child_node, ConfluencePageID(page.id))
|
|
111
105
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
) -> None:
|
|
106
|
+
@override
|
|
107
|
+
def _update_page(self, page_id: ConfluencePageID, document: ConfluenceDocument, path: Path) -> None:
|
|
115
108
|
"""
|
|
116
109
|
Saves a new version of a Confluence document.
|
|
117
110
|
|
|
@@ -119,11 +112,11 @@ class SynchronizingProcessor(Processor):
|
|
|
119
112
|
"""
|
|
120
113
|
|
|
121
114
|
base_path = path.parent
|
|
122
|
-
for
|
|
115
|
+
for image_path in document.images:
|
|
123
116
|
self.api.upload_attachment(
|
|
124
117
|
page_id.page_id,
|
|
125
|
-
attachment_name(
|
|
126
|
-
attachment_path=
|
|
118
|
+
attachment_name(path_relative_to(image_path, base_path)),
|
|
119
|
+
attachment_path=image_path,
|
|
127
120
|
)
|
|
128
121
|
|
|
129
122
|
for name, data in document.embedded_images.items():
|
|
@@ -138,13 +131,9 @@ class SynchronizingProcessor(Processor):
|
|
|
138
131
|
|
|
139
132
|
title = None
|
|
140
133
|
if document.title is not None:
|
|
141
|
-
meta = self.page_metadata
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if meta.overwrite:
|
|
145
|
-
conflicting_page_id = self.api.page_exists(
|
|
146
|
-
document.title, space_id=self.api.space_key_to_id(meta.space_key)
|
|
147
|
-
)
|
|
134
|
+
meta = self.page_metadata.get(path)
|
|
135
|
+
if meta is not None and meta.space_key is not None and meta.title != document.title:
|
|
136
|
+
conflicting_page_id = self.api.page_exists(document.title, space_id=self.api.space_key_to_id(meta.space_key))
|
|
148
137
|
if conflicting_page_id is None:
|
|
149
138
|
title = document.title
|
|
150
139
|
else:
|
|
@@ -156,17 +145,23 @@ class SynchronizingProcessor(Processor):
|
|
|
156
145
|
|
|
157
146
|
self.api.update_page(page_id.page_id, content, title=title)
|
|
158
147
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
148
|
+
if document.labels is not None:
|
|
149
|
+
self.api.update_labels(
|
|
150
|
+
page_id.page_id,
|
|
151
|
+
[ConfluenceLabel(name=label, prefix="global") for label in document.labels],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if document.properties is not None:
|
|
155
|
+
self.api.update_content_properties_for_page(page_id.page_id, [ConfluenceContentProperty(key, value) for key, value in document.properties.items()])
|
|
156
|
+
|
|
157
|
+
def _update_markdown(self, path: Path, *, page_id: str, space_key: str) -> None:
|
|
166
158
|
"""
|
|
167
159
|
Writes the Confluence page ID and space key at the beginning of the Markdown file.
|
|
168
160
|
"""
|
|
169
161
|
|
|
162
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
163
|
+
document = file.read()
|
|
164
|
+
|
|
170
165
|
content: list[str] = []
|
|
171
166
|
|
|
172
167
|
# check if the file has frontmatter
|
|
@@ -178,9 +173,7 @@ class SynchronizingProcessor(Processor):
|
|
|
178
173
|
content.append(document[:index])
|
|
179
174
|
|
|
180
175
|
content.append(f"<!-- confluence-page-id: {page_id} -->")
|
|
181
|
-
|
|
182
|
-
content.append(f"<!-- confluence-space-key: {space_key} -->")
|
|
183
|
-
|
|
176
|
+
content.append(f"<!-- confluence-space-key: {space_key} -->")
|
|
184
177
|
content.append(document[index:])
|
|
185
178
|
|
|
186
179
|
with open(path, "w", encoding="utf-8") as file:
|
|
@@ -190,9 +183,7 @@ class SynchronizingProcessor(Processor):
|
|
|
190
183
|
class SynchronizingProcessorFactory(ProcessorFactory):
|
|
191
184
|
api: ConfluenceSession
|
|
192
185
|
|
|
193
|
-
def __init__(
|
|
194
|
-
self, api: ConfluenceSession, options: ConfluenceDocumentOptions
|
|
195
|
-
) -> None:
|
|
186
|
+
def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions) -> None:
|
|
196
187
|
super().__init__(options, api.site)
|
|
197
188
|
self.api = api
|
|
198
189
|
|
|
@@ -207,7 +198,5 @@ class Application(Converter):
|
|
|
207
198
|
This is the class instantiated by the command-line application.
|
|
208
199
|
"""
|
|
209
200
|
|
|
210
|
-
def __init__(
|
|
211
|
-
self, api: ConfluenceSession, options: ConfluenceDocumentOptions
|
|
212
|
-
) -> None:
|
|
201
|
+
def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions) -> None:
|
|
213
202
|
super().__init__(SynchronizingProcessorFactory(api, options))
|
md2conf/collection.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Publish Markdown files to Confluence wiki.
|
|
3
|
+
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/md2conf
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterable, Optional
|
|
11
|
+
|
|
12
|
+
from .metadata import ConfluencePageMetadata
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfluencePageCollection:
|
|
16
|
+
_metadata: dict[Path, ConfluencePageMetadata]
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._metadata = {}
|
|
20
|
+
|
|
21
|
+
def __len__(self) -> int:
|
|
22
|
+
return len(self._metadata)
|
|
23
|
+
|
|
24
|
+
def add(self, path: Path, data: ConfluencePageMetadata) -> None:
|
|
25
|
+
self._metadata[path] = data
|
|
26
|
+
|
|
27
|
+
def get(self, path: Path) -> Optional[ConfluencePageMetadata]:
|
|
28
|
+
return self._metadata.get(path)
|
|
29
|
+
|
|
30
|
+
def items(self) -> Iterable[tuple[Path, ConfluencePageMetadata]]:
|
|
31
|
+
return self._metadata.items()
|