markdown-to-confluence 0.4.0__py3-none-any.whl → 0.4.2__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.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/METADATA +133 -43
- markdown_to_confluence-0.4.2.dist-info/RECORD +27 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +57 -18
- md2conf/api.py +242 -125
- md2conf/application.py +40 -48
- md2conf/collection.py +17 -11
- md2conf/converter.py +540 -107
- md2conf/drawio.py +222 -0
- md2conf/extra.py +13 -0
- md2conf/local.py +5 -12
- md2conf/matcher.py +64 -7
- md2conf/mermaid.py +2 -7
- md2conf/metadata.py +2 -0
- md2conf/processor.py +48 -57
- md2conf/properties.py +45 -12
- md2conf/scanner.py +17 -9
- md2conf/xml.py +70 -0
- markdown_to_confluence-0.4.0.dist-info/RECORD +0 -25
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/zip-safe +0 -0
md2conf/application.py
CHANGED
|
@@ -10,17 +10,13 @@ import logging
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Optional
|
|
12
12
|
|
|
13
|
-
from .api import ConfluenceLabel, ConfluenceSession
|
|
14
|
-
from .converter import
|
|
15
|
-
|
|
16
|
-
ConfluenceDocumentOptions,
|
|
17
|
-
ConfluencePageID,
|
|
18
|
-
attachment_name,
|
|
19
|
-
)
|
|
20
|
-
from .extra import override
|
|
13
|
+
from .api import ConfluenceContentProperty, ConfluenceLabel, ConfluenceSession, ConfluenceStatus
|
|
14
|
+
from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID, attachment_name, elements_from_string, get_volatile_attributes
|
|
15
|
+
from .extra import override, path_relative_to
|
|
21
16
|
from .metadata import ConfluencePageMetadata
|
|
22
17
|
from .processor import Converter, DocumentNode, Processor, ProcessorFactory
|
|
23
18
|
from .properties import PageError
|
|
19
|
+
from .xml import is_xml_equal
|
|
24
20
|
|
|
25
21
|
LOGGER = logging.getLogger(__name__)
|
|
26
22
|
|
|
@@ -32,9 +28,7 @@ class SynchronizingProcessor(Processor):
|
|
|
32
28
|
|
|
33
29
|
api: ConfluenceSession
|
|
34
30
|
|
|
35
|
-
def __init__(
|
|
36
|
-
self, api: ConfluenceSession, options: ConfluenceDocumentOptions, root_dir: Path
|
|
37
|
-
) -> None:
|
|
31
|
+
def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions, root_dir: Path) -> None:
|
|
38
32
|
"""
|
|
39
33
|
Initializes a new processor instance.
|
|
40
34
|
|
|
@@ -47,9 +41,7 @@ class SynchronizingProcessor(Processor):
|
|
|
47
41
|
self.api = api
|
|
48
42
|
|
|
49
43
|
@override
|
|
50
|
-
def _synchronize_tree(
|
|
51
|
-
self, root: DocumentNode, root_id: Optional[ConfluencePageID]
|
|
52
|
-
) -> None:
|
|
44
|
+
def _synchronize_tree(self, root: DocumentNode, root_id: Optional[ConfluencePageID]) -> None:
|
|
53
45
|
"""
|
|
54
46
|
Creates the cross-reference index and synchronizes the directory tree structure with the Confluence page hierarchy.
|
|
55
47
|
|
|
@@ -59,14 +51,10 @@ class SynchronizingProcessor(Processor):
|
|
|
59
51
|
"""
|
|
60
52
|
|
|
61
53
|
if root.page_id is None and root_id is None:
|
|
62
|
-
raise PageError(
|
|
63
|
-
f"expected: root page ID in options, or explicit page ID in {root.absolute_path}"
|
|
64
|
-
)
|
|
54
|
+
raise PageError(f"expected: root page ID in options, or explicit page ID in {root.absolute_path}")
|
|
65
55
|
elif root.page_id is not None and root_id is not None:
|
|
66
56
|
if root.page_id != root_id.page_id:
|
|
67
|
-
raise PageError(
|
|
68
|
-
f"mismatched inferred page ID of {root_id.page_id} and explicit page ID in {root.absolute_path}"
|
|
69
|
-
)
|
|
57
|
+
raise PageError(f"mismatched inferred page ID of {root_id.page_id} and explicit page ID in {root.absolute_path}")
|
|
70
58
|
|
|
71
59
|
real_id = root_id
|
|
72
60
|
elif root_id is not None:
|
|
@@ -78,9 +66,7 @@ class SynchronizingProcessor(Processor):
|
|
|
78
66
|
|
|
79
67
|
self._synchronize_subtree(root, real_id)
|
|
80
68
|
|
|
81
|
-
def _synchronize_subtree(
|
|
82
|
-
self, node: DocumentNode, parent_id: ConfluencePageID
|
|
83
|
-
) -> None:
|
|
69
|
+
def _synchronize_subtree(self, node: DocumentNode, parent_id: ConfluencePageID) -> None:
|
|
84
70
|
if node.page_id is not None:
|
|
85
71
|
# verify if page exists
|
|
86
72
|
page = self.api.get_page_properties(node.page_id)
|
|
@@ -88,6 +74,10 @@ class SynchronizingProcessor(Processor):
|
|
|
88
74
|
elif node.title is not None:
|
|
89
75
|
# look up page by title
|
|
90
76
|
page = self.api.get_or_create_page(node.title, parent_id.page_id)
|
|
77
|
+
|
|
78
|
+
if page.status is ConfluenceStatus.ARCHIVED:
|
|
79
|
+
raise PageError(f"unable to update archived page with ID {page.id}")
|
|
80
|
+
|
|
91
81
|
update = True
|
|
92
82
|
else:
|
|
93
83
|
# always create a new page
|
|
@@ -108,6 +98,7 @@ class SynchronizingProcessor(Processor):
|
|
|
108
98
|
page_id=page.id,
|
|
109
99
|
space_key=space_key,
|
|
110
100
|
title=page.title,
|
|
101
|
+
synchronized=node.synchronized,
|
|
111
102
|
)
|
|
112
103
|
self.page_metadata.add(node.absolute_path, data)
|
|
113
104
|
|
|
@@ -115,9 +106,7 @@ class SynchronizingProcessor(Processor):
|
|
|
115
106
|
self._synchronize_subtree(child_node, ConfluencePageID(page.id))
|
|
116
107
|
|
|
117
108
|
@override
|
|
118
|
-
def _update_page(
|
|
119
|
-
self, page_id: ConfluencePageID, document: ConfluenceDocument, path: Path
|
|
120
|
-
) -> None:
|
|
109
|
+
def _update_page(self, page_id: ConfluencePageID, document: ConfluenceDocument, path: Path) -> None:
|
|
121
110
|
"""
|
|
122
111
|
Saves a new version of a Confluence document.
|
|
123
112
|
|
|
@@ -125,11 +114,11 @@ class SynchronizingProcessor(Processor):
|
|
|
125
114
|
"""
|
|
126
115
|
|
|
127
116
|
base_path = path.parent
|
|
128
|
-
for
|
|
117
|
+
for image_path in document.images:
|
|
129
118
|
self.api.upload_attachment(
|
|
130
119
|
page_id.page_id,
|
|
131
|
-
attachment_name(
|
|
132
|
-
attachment_path=
|
|
120
|
+
attachment_name(path_relative_to(image_path, base_path)),
|
|
121
|
+
attachment_path=image_path,
|
|
133
122
|
)
|
|
134
123
|
|
|
135
124
|
for name, data in document.embedded_images.items():
|
|
@@ -145,14 +134,8 @@ class SynchronizingProcessor(Processor):
|
|
|
145
134
|
title = None
|
|
146
135
|
if document.title is not None:
|
|
147
136
|
meta = self.page_metadata.get(path)
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
and meta.space_key is not None
|
|
151
|
-
and meta.title != document.title
|
|
152
|
-
):
|
|
153
|
-
conflicting_page_id = self.api.page_exists(
|
|
154
|
-
document.title, space_id=self.api.space_key_to_id(meta.space_key)
|
|
155
|
-
)
|
|
137
|
+
if meta is not None and meta.space_key is not None and meta.title != document.title:
|
|
138
|
+
conflicting_page_id = self.api.page_exists(document.title, space_id=self.api.space_key_to_id(meta.space_key))
|
|
156
139
|
if conflicting_page_id is None:
|
|
157
140
|
title = document.title
|
|
158
141
|
else:
|
|
@@ -162,17 +145,30 @@ class SynchronizingProcessor(Processor):
|
|
|
162
145
|
conflicting_page_id,
|
|
163
146
|
)
|
|
164
147
|
|
|
165
|
-
|
|
148
|
+
# fetch existing page
|
|
149
|
+
page = self.api.get_page(page_id.page_id)
|
|
150
|
+
if not title: # empty or `None`
|
|
151
|
+
title = page.title
|
|
152
|
+
|
|
153
|
+
# check if page has any changes
|
|
154
|
+
if page.title != title or not is_xml_equal(
|
|
155
|
+
document.root,
|
|
156
|
+
elements_from_string(page.content),
|
|
157
|
+
skip_attributes=get_volatile_attributes(),
|
|
158
|
+
):
|
|
159
|
+
self.api.update_page(page_id.page_id, content, title=title, version=page.version.number + 1)
|
|
160
|
+
else:
|
|
161
|
+
LOGGER.info("Up-to-date page: %s", page_id)
|
|
166
162
|
|
|
167
163
|
if document.labels is not None:
|
|
168
164
|
self.api.update_labels(
|
|
169
165
|
page_id.page_id,
|
|
170
|
-
[
|
|
171
|
-
ConfluenceLabel(name=label, prefix="global")
|
|
172
|
-
for label in document.labels
|
|
173
|
-
],
|
|
166
|
+
[ConfluenceLabel(name=label, prefix="global") for label in document.labels],
|
|
174
167
|
)
|
|
175
168
|
|
|
169
|
+
if document.properties is not None:
|
|
170
|
+
self.api.update_content_properties_for_page(page_id.page_id, [ConfluenceContentProperty(key, value) for key, value in document.properties.items()])
|
|
171
|
+
|
|
176
172
|
def _update_markdown(self, path: Path, *, page_id: str, space_key: str) -> None:
|
|
177
173
|
"""
|
|
178
174
|
Writes the Confluence page ID and space key at the beginning of the Markdown file.
|
|
@@ -202,9 +198,7 @@ class SynchronizingProcessor(Processor):
|
|
|
202
198
|
class SynchronizingProcessorFactory(ProcessorFactory):
|
|
203
199
|
api: ConfluenceSession
|
|
204
200
|
|
|
205
|
-
def __init__(
|
|
206
|
-
self, api: ConfluenceSession, options: ConfluenceDocumentOptions
|
|
207
|
-
) -> None:
|
|
201
|
+
def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions) -> None:
|
|
208
202
|
super().__init__(options, api.site)
|
|
209
203
|
self.api = api
|
|
210
204
|
|
|
@@ -219,7 +213,5 @@ class Application(Converter):
|
|
|
219
213
|
This is the class instantiated by the command-line application.
|
|
220
214
|
"""
|
|
221
215
|
|
|
222
|
-
def __init__(
|
|
223
|
-
self, api: ConfluenceSession, options: ConfluenceDocumentOptions
|
|
224
|
-
) -> None:
|
|
216
|
+
def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions) -> None:
|
|
225
217
|
super().__init__(SynchronizingProcessorFactory(api, options))
|
md2conf/collection.py
CHANGED
|
@@ -7,25 +7,31 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Iterable, Optional
|
|
10
|
+
from typing import Generic, Iterable, Optional, TypeVar
|
|
11
11
|
|
|
12
12
|
from .metadata import ConfluencePageMetadata
|
|
13
13
|
|
|
14
|
+
K = TypeVar("K")
|
|
15
|
+
V = TypeVar("V")
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
|
|
18
|
+
class KeyValueCollection(Generic[K, V]):
|
|
19
|
+
_collection: dict[K, V]
|
|
17
20
|
|
|
18
21
|
def __init__(self) -> None:
|
|
19
|
-
self.
|
|
22
|
+
self._collection = {}
|
|
20
23
|
|
|
21
24
|
def __len__(self) -> int:
|
|
22
|
-
return len(self.
|
|
25
|
+
return len(self._collection)
|
|
26
|
+
|
|
27
|
+
def add(self, key: K, data: V) -> None:
|
|
28
|
+
self._collection[key] = data
|
|
29
|
+
|
|
30
|
+
def get(self, key: K) -> Optional[V]:
|
|
31
|
+
return self._collection.get(key)
|
|
23
32
|
|
|
24
|
-
def
|
|
25
|
-
self.
|
|
33
|
+
def items(self) -> Iterable[tuple[K, V]]:
|
|
34
|
+
return self._collection.items()
|
|
26
35
|
|
|
27
|
-
def get(self, path: Path) -> Optional[ConfluencePageMetadata]:
|
|
28
|
-
return self._metadata.get(path)
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
return self._metadata.items()
|
|
37
|
+
class ConfluencePageCollection(KeyValueCollection[Path, ConfluencePageMetadata]): ...
|