markdown-to-confluence 0.4.1__py3-none-any.whl → 0.4.3__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.1.dist-info → markdown_to_confluence-0.4.3.dist-info}/METADATA +96 -32
- markdown_to_confluence-0.4.3.dist-info/RECORD +29 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +94 -15
- md2conf/api.py +84 -94
- md2conf/application.py +18 -2
- md2conf/collection.py +17 -11
- md2conf/converter.py +456 -168
- md2conf/domain.py +46 -0
- md2conf/drawio.py +271 -0
- md2conf/local.py +10 -4
- md2conf/markdown.py +108 -0
- md2conf/matcher.py +63 -4
- md2conf/metadata.py +2 -0
- md2conf/processor.py +39 -29
- md2conf/scanner.py +7 -0
- md2conf/xml.py +70 -0
- markdown_to_confluence-0.4.1.dist-info/RECORD +0 -25
- {markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.3.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.3.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.3.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.3.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.3.dist-info}/zip-safe +0 -0
md2conf/processor.py
CHANGED
|
@@ -14,8 +14,9 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Iterable, Optional
|
|
15
15
|
|
|
16
16
|
from .collection import ConfluencePageCollection
|
|
17
|
-
from .converter import ConfluenceDocument
|
|
18
|
-
from .
|
|
17
|
+
from .converter import ConfluenceDocument
|
|
18
|
+
from .domain import ConfluenceDocumentOptions, ConfluencePageID
|
|
19
|
+
from .matcher import DirectoryEntry, FileEntry, Matcher, MatcherOptions
|
|
19
20
|
from .metadata import ConfluenceSiteMetadata
|
|
20
21
|
from .properties import ArgumentError
|
|
21
22
|
from .scanner import Scanner
|
|
@@ -28,6 +29,7 @@ class DocumentNode:
|
|
|
28
29
|
page_id: Optional[str]
|
|
29
30
|
space_key: Optional[str]
|
|
30
31
|
title: Optional[str]
|
|
32
|
+
synchronized: bool
|
|
31
33
|
|
|
32
34
|
_children: list["DocumentNode"]
|
|
33
35
|
|
|
@@ -35,13 +37,15 @@ class DocumentNode:
|
|
|
35
37
|
self,
|
|
36
38
|
absolute_path: Path,
|
|
37
39
|
page_id: Optional[str],
|
|
38
|
-
space_key: Optional[str]
|
|
39
|
-
title: Optional[str]
|
|
40
|
+
space_key: Optional[str],
|
|
41
|
+
title: Optional[str],
|
|
42
|
+
synchronized: bool,
|
|
40
43
|
):
|
|
41
44
|
self.absolute_path = absolute_path
|
|
42
45
|
self.page_id = page_id
|
|
43
46
|
self.space_key = space_key
|
|
44
47
|
self.title = title
|
|
48
|
+
self.synchronized = synchronized
|
|
45
49
|
self._children = []
|
|
46
50
|
|
|
47
51
|
def count(self) -> int:
|
|
@@ -98,16 +102,11 @@ class Processor:
|
|
|
98
102
|
local_dir = local_dir.resolve(True)
|
|
99
103
|
LOGGER.info("Processing directory: %s", local_dir)
|
|
100
104
|
|
|
101
|
-
#
|
|
105
|
+
# build index of all Markdown files in directory hierarchy
|
|
102
106
|
root = self._index_directory(local_dir, None)
|
|
103
107
|
LOGGER.info("Indexed %d document(s)", root.count())
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
self._synchronize_tree(root, self.options.root_page_id)
|
|
107
|
-
|
|
108
|
-
# Step 3: synchronize files in directory hierarchy with pages in space
|
|
109
|
-
for path, metadata in self.page_metadata.items():
|
|
110
|
-
self._synchronize_page(path, ConfluencePageID(metadata.page_id))
|
|
109
|
+
self._process_items(root)
|
|
111
110
|
|
|
112
111
|
def process_page(self, path: Path) -> None:
|
|
113
112
|
"""
|
|
@@ -115,16 +114,22 @@ class Processor:
|
|
|
115
114
|
"""
|
|
116
115
|
|
|
117
116
|
LOGGER.info("Processing page: %s", path)
|
|
118
|
-
|
|
119
|
-
# Step 1: parse Markdown file
|
|
120
117
|
root = self._index_file(path)
|
|
121
118
|
|
|
122
|
-
|
|
119
|
+
self._process_items(root)
|
|
120
|
+
|
|
121
|
+
def _process_items(self, root: DocumentNode) -> None:
|
|
122
|
+
"""
|
|
123
|
+
Processes a sub-tree rooted at an ancestor node.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
# synchronize directory tree structure with page hierarchy in space (find matching pages in Confluence)
|
|
123
127
|
self._synchronize_tree(root, self.options.root_page_id)
|
|
124
128
|
|
|
125
|
-
#
|
|
129
|
+
# synchronize files in directory hierarchy with pages in space
|
|
126
130
|
for path, metadata in self.page_metadata.items():
|
|
127
|
-
|
|
131
|
+
if metadata.synchronized:
|
|
132
|
+
self._synchronize_page(path, ConfluencePageID(metadata.page_id))
|
|
128
133
|
|
|
129
134
|
def _synchronize_page(self, path: Path, page_id: ConfluencePageID) -> None:
|
|
130
135
|
"""
|
|
@@ -161,36 +166,40 @@ class Processor:
|
|
|
161
166
|
|
|
162
167
|
matcher = Matcher(MatcherOptions(source=".mdignore", extension="md"), local_dir)
|
|
163
168
|
|
|
164
|
-
files: list[
|
|
165
|
-
directories: list[
|
|
169
|
+
files: list[FileEntry] = []
|
|
170
|
+
directories: list[DirectoryEntry] = []
|
|
166
171
|
for entry in os.scandir(local_dir):
|
|
167
172
|
if matcher.is_excluded(entry):
|
|
168
173
|
continue
|
|
169
174
|
|
|
170
175
|
if entry.is_file():
|
|
171
|
-
files.append(
|
|
176
|
+
files.append(FileEntry(entry.name))
|
|
172
177
|
elif entry.is_dir():
|
|
173
|
-
directories.append(
|
|
178
|
+
directories.append(DirectoryEntry(entry.name))
|
|
179
|
+
|
|
180
|
+
files.sort()
|
|
181
|
+
directories.sort()
|
|
174
182
|
|
|
175
183
|
# make page act as parent node
|
|
176
184
|
parent_doc: Optional[Path] = None
|
|
177
|
-
if (
|
|
185
|
+
if FileEntry("index.md") in files:
|
|
178
186
|
parent_doc = local_dir / "index.md"
|
|
179
|
-
elif (
|
|
187
|
+
elif FileEntry("README.md") in files:
|
|
180
188
|
parent_doc = local_dir / "README.md"
|
|
181
|
-
elif (
|
|
189
|
+
elif FileEntry(f"{local_dir.name}.md") in files:
|
|
182
190
|
parent_doc = local_dir / f"{local_dir.name}.md"
|
|
183
191
|
|
|
184
192
|
if parent_doc is None and self.options.keep_hierarchy:
|
|
185
193
|
parent_doc = local_dir / "index.md"
|
|
186
194
|
|
|
187
195
|
# create a blank page for directory entry
|
|
188
|
-
with open(parent_doc, "w"):
|
|
189
|
-
|
|
196
|
+
with open(parent_doc, "w") as f:
|
|
197
|
+
print("[[_LISTING_]]", file=f)
|
|
190
198
|
|
|
191
199
|
if parent_doc is not None:
|
|
192
|
-
|
|
193
|
-
|
|
200
|
+
parent_entry = FileEntry(parent_doc.name)
|
|
201
|
+
if parent_entry in files:
|
|
202
|
+
files.remove(parent_entry)
|
|
194
203
|
|
|
195
204
|
# promote Markdown document in directory as parent page in Confluence
|
|
196
205
|
node = self._index_file(parent_doc)
|
|
@@ -201,11 +210,11 @@ class Processor:
|
|
|
201
210
|
raise ArgumentError(f"root page requires corresponding top-level Markdown document in {local_dir}")
|
|
202
211
|
|
|
203
212
|
for file in files:
|
|
204
|
-
node = self._index_file(file)
|
|
213
|
+
node = self._index_file(local_dir / Path(file.name))
|
|
205
214
|
parent.add_child(node)
|
|
206
215
|
|
|
207
216
|
for directory in directories:
|
|
208
|
-
self._index_directory(directory, parent)
|
|
217
|
+
self._index_directory(local_dir / Path(directory.name), parent)
|
|
209
218
|
|
|
210
219
|
return parent
|
|
211
220
|
|
|
@@ -224,6 +233,7 @@ class Processor:
|
|
|
224
233
|
page_id=document.page_id,
|
|
225
234
|
space_key=document.space_key,
|
|
226
235
|
title=document.title,
|
|
236
|
+
synchronized=document.synchronized if document.synchronized is not None else True,
|
|
227
237
|
)
|
|
228
238
|
|
|
229
239
|
def _generate_hash(self, absolute_path: Path) -> str:
|
md2conf/scanner.py
CHANGED
|
@@ -69,6 +69,7 @@ class DocumentProperties:
|
|
|
69
69
|
:param generated_by: Text identifying the tool that generated the document.
|
|
70
70
|
:param title: The title extracted from front-matter.
|
|
71
71
|
:param tags: A list of tags (content labels) extracted from front-matter.
|
|
72
|
+
:param synchronized: True if the document content is parsed and synchronized with Confluence.
|
|
72
73
|
:param properties: A dictionary of key-value pairs extracted from front-matter to apply as page properties.
|
|
73
74
|
"""
|
|
74
75
|
|
|
@@ -79,6 +80,7 @@ class DocumentProperties:
|
|
|
79
80
|
generated_by: Optional[str]
|
|
80
81
|
title: Optional[str]
|
|
81
82
|
tags: Optional[list[str]]
|
|
83
|
+
synchronized: Optional[bool]
|
|
82
84
|
properties: Optional[dict[str, JsonType]]
|
|
83
85
|
|
|
84
86
|
|
|
@@ -92,6 +94,7 @@ class ScannedDocument:
|
|
|
92
94
|
:param generated_by: Text identifying the tool that generated the document.
|
|
93
95
|
:param title: The title extracted from front-matter.
|
|
94
96
|
:param tags: A list of tags (content labels) extracted from front-matter.
|
|
97
|
+
:param synchronized: True if the document content is parsed and synchronized with Confluence.
|
|
95
98
|
:param properties: A dictionary of key-value pairs extracted from front-matter to apply as page properties.
|
|
96
99
|
:param text: Text that remains after front-matter and inline properties have been extracted.
|
|
97
100
|
"""
|
|
@@ -101,6 +104,7 @@ class ScannedDocument:
|
|
|
101
104
|
generated_by: Optional[str]
|
|
102
105
|
title: Optional[str]
|
|
103
106
|
tags: Optional[list[str]]
|
|
107
|
+
synchronized: Optional[bool]
|
|
104
108
|
properties: Optional[dict[str, JsonType]]
|
|
105
109
|
text: str
|
|
106
110
|
|
|
@@ -126,6 +130,7 @@ class Scanner:
|
|
|
126
130
|
|
|
127
131
|
title: Optional[str] = None
|
|
128
132
|
tags: Optional[list[str]] = None
|
|
133
|
+
synchronized: Optional[bool] = None
|
|
129
134
|
properties: Optional[dict[str, JsonType]] = None
|
|
130
135
|
|
|
131
136
|
# extract front-matter
|
|
@@ -137,6 +142,7 @@ class Scanner:
|
|
|
137
142
|
generated_by = generated_by or p.generated_by
|
|
138
143
|
title = p.title
|
|
139
144
|
tags = p.tags
|
|
145
|
+
synchronized = p.synchronized
|
|
140
146
|
properties = p.properties
|
|
141
147
|
|
|
142
148
|
return ScannedDocument(
|
|
@@ -145,6 +151,7 @@ class Scanner:
|
|
|
145
151
|
generated_by=generated_by,
|
|
146
152
|
title=title,
|
|
147
153
|
tags=tags,
|
|
154
|
+
synchronized=synchronized,
|
|
148
155
|
properties=properties,
|
|
149
156
|
text=text,
|
|
150
157
|
)
|
md2conf/xml.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from typing import Iterable, Optional, Union
|
|
2
|
+
|
|
3
|
+
import lxml.etree as ET
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _attrs_equal_excluding(attrs1: ET._Attrib, attrs2: ET._Attrib, exclude: set[Union[str, ET.QName]]) -> bool:
|
|
7
|
+
"""
|
|
8
|
+
Compares two dictionary objects, excluding keys in the skip set.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# create key sets to compare, excluding keys to be skipped
|
|
12
|
+
keys1 = {k for k in attrs1.keys() if k not in exclude}
|
|
13
|
+
keys2 = {k for k in attrs2.keys() if k not in exclude}
|
|
14
|
+
if keys1 != keys2:
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
# compare values for each key
|
|
18
|
+
for key in keys1:
|
|
19
|
+
if attrs1.get(key) != attrs2.get(key):
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ElementComparator:
|
|
26
|
+
skip_attributes: set[Union[str, ET.QName]]
|
|
27
|
+
|
|
28
|
+
def __init__(self, *, skip_attributes: Optional[Iterable[Union[str, ET.QName]]] = None):
|
|
29
|
+
self.skip_attributes = set(skip_attributes) if skip_attributes else set()
|
|
30
|
+
|
|
31
|
+
def is_equal(self, e1: ET._Element, e2: ET._Element) -> bool:
|
|
32
|
+
"""
|
|
33
|
+
Recursively check if two XML elements are equal.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
if e1.tag != e2.tag:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
e1_text = e1.text.strip() if e1.text else ""
|
|
40
|
+
e2_text = e2.text.strip() if e2.text else ""
|
|
41
|
+
if e1_text != e2_text:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
e1_tail = e1.tail.strip() if e1.tail else ""
|
|
45
|
+
e2_tail = e2.tail.strip() if e2.tail else ""
|
|
46
|
+
if e1_tail != e2_tail:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
if not _attrs_equal_excluding(e1.attrib, e2.attrib, self.skip_attributes):
|
|
50
|
+
return False
|
|
51
|
+
if len(e1) != len(e2):
|
|
52
|
+
return False
|
|
53
|
+
return all(self.is_equal(c1, c2) for c1, c2 in zip(e1, e2))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_xml_equal(
|
|
57
|
+
tree1: ET._Element,
|
|
58
|
+
tree2: ET._Element,
|
|
59
|
+
*,
|
|
60
|
+
skip_attributes: Optional[Iterable[Union[str, ET.QName]]] = None,
|
|
61
|
+
) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Compare two XML documents for equivalence, ignoring leading/trailing whitespace differences and attribute definition order.
|
|
64
|
+
|
|
65
|
+
:param tree1: XML document as an element tree.
|
|
66
|
+
:param tree2: XML document as an element tree.
|
|
67
|
+
:returns: True if equivalent, False otherwise.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
return ElementComparator(skip_attributes=skip_attributes).is_equal(tree1, tree2)
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
markdown_to_confluence-0.4.1.dist-info/licenses/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
2
|
-
md2conf/__init__.py,sha256=K6ZE42N5KJjN5o2GqIFa_lcPZvMMCXPMMRWEkvlmcp0,402
|
|
3
|
-
md2conf/__main__.py,sha256=MJm9U75savKWKYm4dLREqlsyBWEqkTtaM4YTWkEeo0E,8388
|
|
4
|
-
md2conf/api.py,sha256=RQ_nb0Z0VnhJma1BU9ABeb4MQZvZEfFS5mTXXKcY6bk,37584
|
|
5
|
-
md2conf/application.py,sha256=cXYXYdEdmMXwhxF69eUiPPG2Ixt4xtlWHXa28wTq150,7571
|
|
6
|
-
md2conf/collection.py,sha256=EAXuIFcIRBO-Giic2hdU2d4Hpj0_ZFBAWI3aKQ2fjrI,775
|
|
7
|
-
md2conf/converter.py,sha256=x2LAY1Hpw5mTVFNJE5_Zm-o7p5y6TTds6KfrpdM5qQk,38823
|
|
8
|
-
md2conf/emoji.py,sha256=UzDrxqFo59wHmbbJmMNdn0rYFDXbZE4qirOM-_egzXc,2603
|
|
9
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
10
|
-
md2conf/extra.py,sha256=VuMxuOnnC2Qwy6y52ukIxsaYhrZArRqMmRHRE4QZl8g,687
|
|
11
|
-
md2conf/local.py,sha256=MVwGxy_n00uqCInLK8FVGaaVnaOp1nfn28PVrWz3cCQ,3496
|
|
12
|
-
md2conf/matcher.py,sha256=izgL_MAMqbXjKPvAz3KpFv5OTDsaJ9GplTJbixrT3oY,4918
|
|
13
|
-
md2conf/mermaid.py,sha256=f0x7ISj-41ZMh4zTAFPhIWwr94SDcsVZUc1NWqmH_G4,2508
|
|
14
|
-
md2conf/metadata.py,sha256=TxgUrskqsWor_pvlQx-p86C0-0qRJ2aeQhuDcXU9Dpc,886
|
|
15
|
-
md2conf/processor.py,sha256=yWVRYtbc9UHSUfRxqyPDsgeVqO7gx0s3RiGL1GzMotE,9405
|
|
16
|
-
md2conf/properties.py,sha256=RC1jY_TKVbOv2bJxXn27Fj4fNWzyoNUQt6ltgUyVQAQ,3987
|
|
17
|
-
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
18
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
md2conf/scanner.py,sha256=qXfnJkaEwDbz6G6Z9llqifBp2TLAlrXAIP4qkCbGdWo,4964
|
|
20
|
-
markdown_to_confluence-0.4.1.dist-info/METADATA,sha256=rAXtL2mR1LHmc_pwkmnwrGpIDMEw-7kZjIJOnMi-NLA,24864
|
|
21
|
-
markdown_to_confluence-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
-
markdown_to_confluence-0.4.1.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
23
|
-
markdown_to_confluence-0.4.1.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
24
|
-
markdown_to_confluence-0.4.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
25
|
-
markdown_to_confluence-0.4.1.dist-info/RECORD,,
|
|
File without changes
|
{markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.3.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.3.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|