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.
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
- if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
46
- raise ArgumentError(
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,,