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/properties.py
CHANGED
|
@@ -7,7 +7,7 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
|
-
from typing import Optional
|
|
10
|
+
from typing import Optional, overload
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class ArgumentError(ValueError):
|
|
@@ -22,6 +22,42 @@ class ConfluenceError(RuntimeError):
|
|
|
22
22
|
"Raised when a Confluence API call fails."
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
@overload
|
|
26
|
+
def _validate_domain(domain: str) -> str: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@overload
|
|
30
|
+
def _validate_domain(domain: Optional[str]) -> Optional[str]: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _validate_domain(domain: Optional[str]) -> Optional[str]:
|
|
34
|
+
if domain is None:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
if domain.startswith(("http://", "https://")) or domain.endswith("/"):
|
|
38
|
+
raise ArgumentError("Confluence domain looks like a URL; only host name required")
|
|
39
|
+
|
|
40
|
+
return domain
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@overload
|
|
44
|
+
def _validate_base_path(base_path: str) -> str: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@overload
|
|
48
|
+
def _validate_base_path(base_path: Optional[str]) -> Optional[str]: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _validate_base_path(base_path: Optional[str]) -> Optional[str]:
|
|
52
|
+
if base_path is None:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
if not base_path.startswith("/") or not base_path.endswith("/"):
|
|
56
|
+
raise ArgumentError("Confluence base path must start and end with a '/'")
|
|
57
|
+
|
|
58
|
+
return base_path
|
|
59
|
+
|
|
60
|
+
|
|
25
61
|
class ConfluenceSiteProperties:
|
|
26
62
|
domain: str
|
|
27
63
|
base_path: str
|
|
@@ -42,15 +78,8 @@ class ConfluenceSiteProperties:
|
|
|
42
78
|
if not opt_base_path:
|
|
43
79
|
opt_base_path = "/wiki/"
|
|
44
80
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"Confluence domain looks like a URL; only host name required"
|
|
48
|
-
)
|
|
49
|
-
if not opt_base_path.startswith("/") or not opt_base_path.endswith("/"):
|
|
50
|
-
raise ArgumentError("Confluence base path must start and end with a '/'")
|
|
51
|
-
|
|
52
|
-
self.domain = opt_domain
|
|
53
|
-
self.base_path = opt_base_path
|
|
81
|
+
self.domain = _validate_domain(opt_domain)
|
|
82
|
+
self.base_path = _validate_base_path(opt_base_path)
|
|
54
83
|
self.space_key = opt_space_key
|
|
55
84
|
|
|
56
85
|
|
|
@@ -92,10 +121,14 @@ class ConfluenceConnectionProperties:
|
|
|
92
121
|
|
|
93
122
|
if not opt_api_key:
|
|
94
123
|
raise ArgumentError("Confluence API key not specified")
|
|
124
|
+
if not opt_api_url and not opt_domain:
|
|
125
|
+
raise ArgumentError("Confluence API URL or domain required")
|
|
126
|
+
if not opt_api_url and not opt_base_path:
|
|
127
|
+
opt_base_path = "/wiki/"
|
|
95
128
|
|
|
96
129
|
self.api_url = opt_api_url
|
|
97
|
-
self.domain = opt_domain
|
|
98
|
-
self.base_path = opt_base_path
|
|
130
|
+
self.domain = _validate_domain(opt_domain)
|
|
131
|
+
self.base_path = _validate_base_path(opt_base_path)
|
|
99
132
|
self.space_key = opt_space_key
|
|
100
133
|
self.user_name = opt_user_name
|
|
101
134
|
self.api_key = opt_api_key
|
md2conf/scanner.py
CHANGED
|
@@ -69,6 +69,8 @@ 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.
|
|
73
|
+
:param properties: A dictionary of key-value pairs extracted from front-matter to apply as page properties.
|
|
72
74
|
"""
|
|
73
75
|
|
|
74
76
|
page_id: Optional[str]
|
|
@@ -78,6 +80,8 @@ class DocumentProperties:
|
|
|
78
80
|
generated_by: Optional[str]
|
|
79
81
|
title: Optional[str]
|
|
80
82
|
tags: Optional[list[str]]
|
|
83
|
+
synchronized: Optional[bool]
|
|
84
|
+
properties: Optional[dict[str, JsonType]]
|
|
81
85
|
|
|
82
86
|
|
|
83
87
|
@dataclass
|
|
@@ -90,6 +94,8 @@ class ScannedDocument:
|
|
|
90
94
|
:param generated_by: Text identifying the tool that generated the document.
|
|
91
95
|
:param title: The title extracted from front-matter.
|
|
92
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.
|
|
98
|
+
:param properties: A dictionary of key-value pairs extracted from front-matter to apply as page properties.
|
|
93
99
|
:param text: Text that remains after front-matter and inline properties have been extracted.
|
|
94
100
|
"""
|
|
95
101
|
|
|
@@ -98,6 +104,8 @@ class ScannedDocument:
|
|
|
98
104
|
generated_by: Optional[str]
|
|
99
105
|
title: Optional[str]
|
|
100
106
|
tags: Optional[list[str]]
|
|
107
|
+
synchronized: Optional[bool]
|
|
108
|
+
properties: Optional[dict[str, JsonType]]
|
|
101
109
|
text: str
|
|
102
110
|
|
|
103
111
|
|
|
@@ -112,22 +120,18 @@ class Scanner:
|
|
|
112
120
|
text = f.read()
|
|
113
121
|
|
|
114
122
|
# extract Confluence page ID
|
|
115
|
-
page_id, text = extract_value(
|
|
116
|
-
r"<!--\s+confluence[-_]page[-_]id:\s*(\d+)\s+-->", text
|
|
117
|
-
)
|
|
123
|
+
page_id, text = extract_value(r"<!--\s+confluence[-_]page[-_]id:\s*(\d+)\s+-->", text)
|
|
118
124
|
|
|
119
125
|
# extract Confluence space key
|
|
120
|
-
space_key, text = extract_value(
|
|
121
|
-
r"<!--\s+confluence[-_]space[-_]key:\s*(\S+)\s+-->", text
|
|
122
|
-
)
|
|
126
|
+
space_key, text = extract_value(r"<!--\s+confluence[-_]space[-_]key:\s*(\S+)\s+-->", text)
|
|
123
127
|
|
|
124
128
|
# extract 'generated-by' tag text
|
|
125
|
-
generated_by, text = extract_value(
|
|
126
|
-
r"<!--\s+generated[-_]by:\s*(.*)\s+-->", text
|
|
127
|
-
)
|
|
129
|
+
generated_by, text = extract_value(r"<!--\s+generated[-_]by:\s*(.*)\s+-->", text)
|
|
128
130
|
|
|
129
131
|
title: Optional[str] = None
|
|
130
132
|
tags: Optional[list[str]] = None
|
|
133
|
+
synchronized: Optional[bool] = None
|
|
134
|
+
properties: Optional[dict[str, JsonType]] = None
|
|
131
135
|
|
|
132
136
|
# extract front-matter
|
|
133
137
|
data, text = extract_frontmatter_properties(text)
|
|
@@ -138,6 +142,8 @@ class Scanner:
|
|
|
138
142
|
generated_by = generated_by or p.generated_by
|
|
139
143
|
title = p.title
|
|
140
144
|
tags = p.tags
|
|
145
|
+
synchronized = p.synchronized
|
|
146
|
+
properties = p.properties
|
|
141
147
|
|
|
142
148
|
return ScannedDocument(
|
|
143
149
|
page_id=page_id,
|
|
@@ -145,5 +151,7 @@ class Scanner:
|
|
|
145
151
|
generated_by=generated_by,
|
|
146
152
|
title=title,
|
|
147
153
|
tags=tags,
|
|
154
|
+
synchronized=synchronized,
|
|
155
|
+
properties=properties,
|
|
148
156
|
text=text,
|
|
149
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.0.dist-info/licenses/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
2
|
-
md2conf/__init__.py,sha256=wwC9K_CM_n25aE2PFfbtbHwaeTRU_l4ZfhrTJGYfRyY,402
|
|
3
|
-
md2conf/__main__.py,sha256=GU_ikdNh2a3gZqOwM6GLR8qM3moeLXaNydQ052G5Pw0,8423
|
|
4
|
-
md2conf/api.py,sha256=kJa4VTNjRwVydasW1YwJeRPI0OrRxiWnxh3OvusCA3o,31985
|
|
5
|
-
md2conf/application.py,sha256=AMVqPQ3A_jUm9ao3M85Ew9MEyS29ar950aHtk_kp6_U,7460
|
|
6
|
-
md2conf/collection.py,sha256=EAXuIFcIRBO-Giic2hdU2d4Hpj0_ZFBAWI3aKQ2fjrI,775
|
|
7
|
-
md2conf/converter.py,sha256=odmoDU_0_ttm2xcC36kTA4w9AptI61lDoNAMUMLj2jg,37553
|
|
8
|
-
md2conf/emoji.py,sha256=UzDrxqFo59wHmbbJmMNdn0rYFDXbZE4qirOM-_egzXc,2603
|
|
9
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
10
|
-
md2conf/extra.py,sha256=Y7_cL7ff9WdhMTHK13ZKjgA19UXJgBT-zIgFv2V57M0,309
|
|
11
|
-
md2conf/local.py,sha256=Uk7x5-jr56BctuUBJoUXgbz2qtES-qqMV27x9nh0280,3584
|
|
12
|
-
md2conf/matcher.py,sha256=y5WEZNklTpUoJtMJlulTvfhl_v-UMU6wySJAKit91ig,4940
|
|
13
|
-
md2conf/mermaid.py,sha256=ZETocFDKi_fSYyVR1pJ7fo207YYFSuT44MSYFQ8-cZ0,2562
|
|
14
|
-
md2conf/metadata.py,sha256=TxgUrskqsWor_pvlQx-p86C0-0qRJ2aeQhuDcXU9Dpc,886
|
|
15
|
-
md2conf/processor.py,sha256=8lRM1s0u1O9fBH2cVsGrfLshwDfjvePxSJB8SEZLcJ4,9815
|
|
16
|
-
md2conf/properties.py,sha256=Z0T9VkEtnPNoRviX1SIrwUJC5cezl-Mv6wmVKxGZdX8,3205
|
|
17
|
-
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
18
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
md2conf/scanner.py,sha256=hkseFV6dWJrKKBhBt9fzKtKliOyDMDkumEVrZF3q1N4,4584
|
|
20
|
-
markdown_to_confluence-0.4.0.dist-info/METADATA,sha256=qeCHbGT8sUwgi11bKYMFFi2UmFa3DyFBCZlzjftHyxs,23573
|
|
21
|
-
markdown_to_confluence-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
-
markdown_to_confluence-0.4.0.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
23
|
-
markdown_to_confluence-0.4.0.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
24
|
-
markdown_to_confluence-0.4.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
25
|
-
markdown_to_confluence-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
{markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|