mkdocs2confluence 0.20.2__tar.gz → 0.20.3__tar.gz
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.
- {mkdocs2confluence-0.20.2/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.20.3}/PKG-INFO +1 -1
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/pyproject.toml +1 -1
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3/src/mkdocs2confluence.egg-info}/PKG-INFO +1 -1
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/emitter/xhtml.py +19 -24
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/client.py +22 -18
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/http_retry.py +1 -1
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_emitter.py +14 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_publish_client.py +46 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/LICENSE +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/README.md +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/setup.cfg +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/cli.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/compiler/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/compiler/models.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/compiler/page.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/loader/config.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/loader/nav.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/loader/page.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/pdf/render.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/captions.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preview/render.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preview/server.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/changelog.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/executor.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/models.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/planner.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/skill_installer.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/skills/mkdocs-changelog/SKILL.md +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/skills/mkdocs-changelog/scripts/changelog_data.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/command.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/comments.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/github.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/platform.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/state.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/_kroki.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/admonition_titles.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/attachment_previews.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/captions.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/images.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/plantuml.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_abbrevs.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_admonition_titles.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_attachment_previews.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_captions.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_changelog_config.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_changelog_publish.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_children_macro.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_cli.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_editlink.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_exclude_properties_config.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_extra_css.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_footer.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_frontmatter.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_icons.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_images.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_internallinks.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_ir.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_linkdefs.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_loader.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_mermaid.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_page_loader.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_parser.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_pdf.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_plantuml.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_preprocess.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_preview.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_publish_config.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_publish_pipeline.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_server.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_skill_installer.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_sync_anchoring.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_sync_command.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_sync_comments.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_sync_github.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_sync_state.py +0 -0
- {mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_treeutil.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mkdocs2confluence"
|
|
3
|
-
version = "0.20.
|
|
3
|
+
version = "0.20.3"
|
|
4
4
|
description = "Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "GPL-3.0-or-later" }
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/emitter/xhtml.py
RENAMED
|
@@ -258,13 +258,9 @@ def _emit_section(node: Section) -> str:
|
|
|
258
258
|
# Confluence's auto heading-anchors use a different naming scheme than the
|
|
259
259
|
# MkDocs slug our links carry, so without an explicit named anchor a
|
|
260
260
|
# `#slug` link lands at the top of the page instead of the section.
|
|
261
|
-
name =
|
|
261
|
+
name = node.explicit_anchor or node.anchor
|
|
262
262
|
if name:
|
|
263
|
-
anchor = (
|
|
264
|
-
f'<ac:structured-macro ac:name="anchor">'
|
|
265
|
-
f'<ac:parameter ac:name=""><![CDATA[{name}]]></ac:parameter>'
|
|
266
|
-
f"</ac:structured-macro>"
|
|
267
|
-
)
|
|
263
|
+
anchor = _anchor_macro(name)
|
|
268
264
|
heading = f"{anchor}<{tag}{style_attr}>{title_html}</{tag}>\n"
|
|
269
265
|
body = emit(node.children)
|
|
270
266
|
return heading + body
|
|
@@ -303,6 +299,20 @@ def _emit_front_matter(node: FrontMatter) -> str:
|
|
|
303
299
|
return "".join(parts)
|
|
304
300
|
|
|
305
301
|
|
|
302
|
+
def _anchor_macro(name: str) -> str:
|
|
303
|
+
"""Confluence anchor macro for *name*.
|
|
304
|
+
|
|
305
|
+
The name sits in CDATA, which is not entity-decoded, so it must not be
|
|
306
|
+
HTML-escaped — only a literal ``]]>`` needs splitting.
|
|
307
|
+
"""
|
|
308
|
+
safe = name.replace("]]>", "]]]]><![CDATA[>")
|
|
309
|
+
return (
|
|
310
|
+
f'<ac:structured-macro ac:name="anchor">'
|
|
311
|
+
f'<ac:parameter ac:name=""><![CDATA[{safe}]]></ac:parameter>'
|
|
312
|
+
f"</ac:structured-macro>"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
306
316
|
def _xml_safe(text: str) -> str:
|
|
307
317
|
"""HTML-escape *text* and encode non-ASCII as XML numeric character references.
|
|
308
318
|
|
|
@@ -694,12 +704,7 @@ def _emit_abbrev_glossary_block(node: AbbrevGlossaryBlock) -> str:
|
|
|
694
704
|
"""End-of-page abbreviations list with Confluence anchor targets."""
|
|
695
705
|
parts: list[str] = ["<hr />\n<h6>Abbreviations</h6>\n<ol>\n"]
|
|
696
706
|
for fn in node.footnoted:
|
|
697
|
-
|
|
698
|
-
anchor_macro = (
|
|
699
|
-
f'<ac:structured-macro ac:name="anchor">'
|
|
700
|
-
f'<ac:parameter ac:name=""><![CDATA[{anchor}]]></ac:parameter>'
|
|
701
|
-
f"</ac:structured-macro>"
|
|
702
|
-
)
|
|
707
|
+
anchor_macro = _anchor_macro(f"abbr-{fn.number}")
|
|
703
708
|
abbr = html.escape(fn.abbr)
|
|
704
709
|
defn = _emit_inlines(fn.definition)
|
|
705
710
|
parts.append(f"<li>{anchor_macro}<strong>{abbr}</strong> — {defn}</li>\n")
|
|
@@ -714,12 +719,7 @@ def _emit_footnote_block(node: FootnoteBlock) -> str:
|
|
|
714
719
|
"""Footnotes section: heading + ordered list with anchor targets."""
|
|
715
720
|
items: list[str] = []
|
|
716
721
|
for fn in node.items:
|
|
717
|
-
|
|
718
|
-
anchor_macro = (
|
|
719
|
-
f'<ac:structured-macro ac:name="anchor">'
|
|
720
|
-
f'<ac:parameter ac:name=""><![CDATA[{anchor}]]></ac:parameter>'
|
|
721
|
-
f"</ac:structured-macro>"
|
|
722
|
-
)
|
|
722
|
+
anchor_macro = _anchor_macro(f"fn-{fn.label}")
|
|
723
723
|
content = _emit_inlines(fn.children)
|
|
724
724
|
items.append(f"<li>{anchor_macro}{content}</li>\n")
|
|
725
725
|
return "<h2>Footnotes</h2>\n<ol>\n" + "".join(items) + "</ol>\n"
|
|
@@ -780,12 +780,7 @@ def _emit_inline(node: IRNode) -> str:
|
|
|
780
780
|
if isinstance(node, RawInlineHtml):
|
|
781
781
|
return node.html_str
|
|
782
782
|
if isinstance(node, AnchorNode):
|
|
783
|
-
|
|
784
|
-
return (
|
|
785
|
-
f'<ac:structured-macro ac:name="anchor">'
|
|
786
|
-
f"<ac:parameter ac:name=\"\"><![CDATA[{name}]]></ac:parameter>"
|
|
787
|
-
f"</ac:structured-macro>"
|
|
788
|
-
)
|
|
783
|
+
return _anchor_macro(node.name)
|
|
789
784
|
# Fallback: emit unknown inline nodes as escaped repr
|
|
790
785
|
return html.escape(repr(node))
|
|
791
786
|
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/client.py
RENAMED
|
@@ -67,6 +67,10 @@ class ConfluenceClient:
|
|
|
67
67
|
raise RuntimeError("ConfluenceClient must be used as a context manager.")
|
|
68
68
|
return self._client
|
|
69
69
|
|
|
70
|
+
def _get(self, url: str, **kwargs: Any) -> httpx.Response:
|
|
71
|
+
# Reads are rate-limited too; retry 429s like writes do.
|
|
72
|
+
return http_request_with_retry(lambda: self._http.get(url, **kwargs), f"GET {url}")
|
|
73
|
+
|
|
70
74
|
def _base(self) -> str:
|
|
71
75
|
# Strip trailing slash and any trailing /wiki so users can supply either
|
|
72
76
|
# "https://org.atlassian.net" or "https://org.atlassian.net/wiki".
|
|
@@ -95,7 +99,7 @@ class ConfluenceClient:
|
|
|
95
99
|
Raises :class:`ConfluenceError` when the space is not found.
|
|
96
100
|
"""
|
|
97
101
|
url = self._v2("/spaces")
|
|
98
|
-
resp = self.
|
|
102
|
+
resp = self._get(url, params={"keys": space_key, "limit": 1})
|
|
99
103
|
self._raise_for_status(resp, f"get_space_id({space_key!r})")
|
|
100
104
|
data = resp.json()
|
|
101
105
|
results = data.get("results", [])
|
|
@@ -112,7 +116,7 @@ class ConfluenceClient:
|
|
|
112
116
|
Raises :class:`ConfluenceError` when the page is not found.
|
|
113
117
|
"""
|
|
114
118
|
url = self._v2(f"/pages/{page_id}")
|
|
115
|
-
resp = self.
|
|
119
|
+
resp = self._get(url)
|
|
116
120
|
if resp.status_code == 404:
|
|
117
121
|
raise ConfluenceError(
|
|
118
122
|
f"Parent page {page_id!r} not found (HTTP 404). "
|
|
@@ -147,7 +151,7 @@ class ConfluenceClient:
|
|
|
147
151
|
else:
|
|
148
152
|
url = self._v2(f"/pages/{parent_id}/direct-children")
|
|
149
153
|
|
|
150
|
-
resp = self.
|
|
154
|
+
resp = self._get(url, params={"limit": 250})
|
|
151
155
|
self._raise_for_status(resp, f"find_folder_under({title!r})")
|
|
152
156
|
for item in resp.json().get("results", []):
|
|
153
157
|
if item.get("type") == "folder" and item.get("title") == title:
|
|
@@ -168,7 +172,7 @@ class ConfluenceClient:
|
|
|
168
172
|
if not space_key:
|
|
169
173
|
return None
|
|
170
174
|
cql = f'type=folder AND title="{title}" AND space="{space_key}"'
|
|
171
|
-
resp = self.
|
|
175
|
+
resp = self._get(
|
|
172
176
|
self._v1("/content/search"),
|
|
173
177
|
params={"cql": cql, "limit": 10},
|
|
174
178
|
)
|
|
@@ -218,7 +222,7 @@ class ConfluenceClient:
|
|
|
218
222
|
return pages from other spaces.
|
|
219
223
|
"""
|
|
220
224
|
url = self._v2(f"/spaces/{space_id}/pages")
|
|
221
|
-
resp = self.
|
|
225
|
+
resp = self._get(
|
|
222
226
|
url,
|
|
223
227
|
params={"title": title, "status": "current", "limit": 10},
|
|
224
228
|
)
|
|
@@ -303,7 +307,7 @@ class ConfluenceClient:
|
|
|
303
307
|
"""
|
|
304
308
|
key = "content-appearance-published"
|
|
305
309
|
prop_url = self._v1(f"/content/{page_id}/property/{key}")
|
|
306
|
-
get_resp = self.
|
|
310
|
+
get_resp = self._get(prop_url)
|
|
307
311
|
|
|
308
312
|
if get_resp.status_code == 200:
|
|
309
313
|
current_version = get_resp.json().get("version", {}).get("number", 1)
|
|
@@ -330,7 +334,7 @@ class ConfluenceClient:
|
|
|
330
334
|
can safely treat a missing hash as "unknown — must update".
|
|
331
335
|
"""
|
|
332
336
|
url = self._v1(f"/content/{page_id}/property/mk2conf-content-hash")
|
|
333
|
-
resp = self.
|
|
337
|
+
resp = self._get(url)
|
|
334
338
|
if resp.is_error:
|
|
335
339
|
return None
|
|
336
340
|
value = resp.json().get("value", "")
|
|
@@ -345,7 +349,7 @@ class ConfluenceClient:
|
|
|
345
349
|
"""
|
|
346
350
|
key = "mk2conf-content-hash"
|
|
347
351
|
prop_url = self._v1(f"/content/{page_id}/property/{key}")
|
|
348
|
-
get_resp = self.
|
|
352
|
+
get_resp = self._get(prop_url)
|
|
349
353
|
if get_resp.status_code == 200:
|
|
350
354
|
current_version = get_resp.json().get("version", {}).get("number", 1)
|
|
351
355
|
http_request_with_retry(
|
|
@@ -373,7 +377,7 @@ class ConfluenceClient:
|
|
|
373
377
|
label_url = self._v1(f"/content/{page_id}/label")
|
|
374
378
|
|
|
375
379
|
# Remove all existing labels
|
|
376
|
-
existing_resp = self.
|
|
380
|
+
existing_resp = self._get(label_url)
|
|
377
381
|
if existing_resp.status_code == 200:
|
|
378
382
|
for lbl in existing_resp.json().get("results", []):
|
|
379
383
|
name = lbl.get("name", "")
|
|
@@ -453,7 +457,7 @@ class ConfluenceClient:
|
|
|
453
457
|
Returns the ``spaceContentStates`` list, which is the same for every
|
|
454
458
|
page in the space.
|
|
455
459
|
"""
|
|
456
|
-
resp = self.
|
|
460
|
+
resp = self._get(self._v1(f"/content/{page_id}/state/available"))
|
|
457
461
|
if resp.is_success:
|
|
458
462
|
data: dict[str, Any] = resp.json()
|
|
459
463
|
return list(data.get("spaceContentStates") or [])
|
|
@@ -466,7 +470,7 @@ class ConfluenceClient:
|
|
|
466
470
|
Uses the v2 ``GET /pages/{id}/attachments`` endpoint.
|
|
467
471
|
"""
|
|
468
472
|
url = self._v2(f"/pages/{page_id}/attachments")
|
|
469
|
-
resp = self.
|
|
473
|
+
resp = self._get(url, params={"limit": 250})
|
|
470
474
|
self._raise_for_status(resp, f"list_attachments({page_id!r})")
|
|
471
475
|
results: list[dict[str, Any]] = resp.json().get("results", [])
|
|
472
476
|
return {r["title"]: r for r in results}
|
|
@@ -533,7 +537,7 @@ class ConfluenceClient:
|
|
|
533
537
|
a publish.
|
|
534
538
|
"""
|
|
535
539
|
url = self._v2(f"/pages/{page_id}/properties/mk2conf-managed")
|
|
536
|
-
get_resp = self.
|
|
540
|
+
get_resp = self._get(url)
|
|
537
541
|
if get_resp.status_code == 200:
|
|
538
542
|
return # already stamped
|
|
539
543
|
http_request_with_retry(
|
|
@@ -555,7 +559,7 @@ class ConfluenceClient:
|
|
|
555
559
|
url = self._v2(f"/pages/{page_id}/descendants")
|
|
556
560
|
params: dict[str, str | int] = {"depth": "all", "limit": 250}
|
|
557
561
|
while True:
|
|
558
|
-
resp = self.
|
|
562
|
+
resp = self._get(url, params=params)
|
|
559
563
|
self._raise_for_status(resp, f"get_descendant_ids({page_id!r})")
|
|
560
564
|
data = resp.json()
|
|
561
565
|
for item in data.get("results", []):
|
|
@@ -571,7 +575,7 @@ class ConfluenceClient:
|
|
|
571
575
|
|
|
572
576
|
def is_managed(self, page_id: str) -> bool:
|
|
573
577
|
"""Return ``True`` if *page_id* has the ``mk2conf-managed`` property."""
|
|
574
|
-
resp = self.
|
|
578
|
+
resp = self._get(self._v2(f"/pages/{page_id}/properties/mk2conf-managed"))
|
|
575
579
|
return resp.status_code == 200
|
|
576
580
|
|
|
577
581
|
def delete_page(self, page_id: str) -> None:
|
|
@@ -590,7 +594,7 @@ class ConfluenceClient:
|
|
|
590
594
|
url = self._v2(f"/pages/{page_id}/inline-comments")
|
|
591
595
|
params: dict[str, Any] = {"resolution-status": "open", "body-format": "storage", "limit": 250}
|
|
592
596
|
while True:
|
|
593
|
-
resp = self.
|
|
597
|
+
resp = self._get(url, params=params)
|
|
594
598
|
self._raise_for_status(resp, f"get_page_inline_comments({page_id!r})")
|
|
595
599
|
data = resp.json()
|
|
596
600
|
results.extend(data.get("results", []))
|
|
@@ -607,7 +611,7 @@ class ConfluenceClient:
|
|
|
607
611
|
url = self._v2(f"/pages/{page_id}/footer-comments")
|
|
608
612
|
params: dict[str, Any] = {"resolution-status": "open", "body-format": "storage", "limit": 250}
|
|
609
613
|
while True:
|
|
610
|
-
resp = self.
|
|
614
|
+
resp = self._get(url, params=params)
|
|
611
615
|
self._raise_for_status(resp, f"get_page_footer_comments({page_id!r})")
|
|
612
616
|
data = resp.json()
|
|
613
617
|
results.extend(data.get("results", []))
|
|
@@ -635,7 +639,7 @@ class ConfluenceClient:
|
|
|
635
639
|
def resolve_inline_comment(self, comment_id: str) -> None:
|
|
636
640
|
"""Resolve an inline comment by setting *resolved=true*."""
|
|
637
641
|
url = self._v2(f"/inline-comments/{comment_id}")
|
|
638
|
-
get_resp = self.
|
|
642
|
+
get_resp = self._get(url, params={"body-format": "storage"})
|
|
639
643
|
self._raise_for_status(get_resp, f"get_inline_comment({comment_id!r})")
|
|
640
644
|
data = get_resp.json()
|
|
641
645
|
version = data.get("version", {}).get("number", 1)
|
|
@@ -650,7 +654,7 @@ class ConfluenceClient:
|
|
|
650
654
|
def resolve_footer_comment(self, comment_id: str) -> None:
|
|
651
655
|
"""Resolve a footer comment by setting *resolved=true*."""
|
|
652
656
|
url = self._v2(f"/footer-comments/{comment_id}")
|
|
653
|
-
get_resp = self.
|
|
657
|
+
get_resp = self._get(url, params={"body-format": "storage"})
|
|
654
658
|
self._raise_for_status(get_resp, f"get_footer_comment({comment_id!r})")
|
|
655
659
|
data = get_resp.json()
|
|
656
660
|
version = data.get("version", {}).get("number", 1)
|
|
@@ -38,7 +38,7 @@ def http_request_with_retry(fn: Callable[[], httpx.Response], context: str) -> h
|
|
|
38
38
|
)
|
|
39
39
|
header = resp.headers.get("Retry-After", "")
|
|
40
40
|
try:
|
|
41
|
-
wait = min(float(header), _RETRY_AFTER_CAP)
|
|
41
|
+
wait = min(max(float(header), 0.0), _RETRY_AFTER_CAP)
|
|
42
42
|
except ValueError:
|
|
43
43
|
wait = min(2.0 ** attempt + random.uniform(0.0, 1.0), _RETRY_AFTER_CAP)
|
|
44
44
|
print(
|
|
@@ -113,6 +113,20 @@ class TestSectionEmitter:
|
|
|
113
113
|
assert "{ #test-me }" not in out
|
|
114
114
|
assert "<h2>Test me</h2>" in out
|
|
115
115
|
|
|
116
|
+
def test_anchor_name_not_html_escaped_in_cdata(self) -> None:
|
|
117
|
+
# CDATA is not entity-decoded, so escaping the name there would make it
|
|
118
|
+
# differ from the (attribute-decoded) ac:anchor on links pointing at it.
|
|
119
|
+
from mkdocs_to_confluence.ir.nodes import AnchorNode
|
|
120
|
+
|
|
121
|
+
out = emit((Paragraph((AnchorNode(name="it's"),)),))
|
|
122
|
+
assert "<![CDATA[it's]]>" in out
|
|
123
|
+
|
|
124
|
+
def test_anchor_name_splits_cdata_terminator(self) -> None:
|
|
125
|
+
from mkdocs_to_confluence.ir.nodes import AnchorNode
|
|
126
|
+
|
|
127
|
+
out = emit((Paragraph((AnchorNode(name="a]]>b"),)),))
|
|
128
|
+
assert "<![CDATA[a]]]]><![CDATA[>b]]>" in out
|
|
129
|
+
|
|
116
130
|
|
|
117
131
|
class TestParagraphEmitter:
|
|
118
132
|
def test_simple_paragraph(self) -> None:
|
|
@@ -1061,3 +1061,49 @@ def test_request_prints_warning_each_retry(monkeypatch: pytest.MonkeyPatch, caps
|
|
|
1061
1061
|
assert out.count("rate-limited") == 2
|
|
1062
1062
|
assert "attempt 1/3" in out
|
|
1063
1063
|
assert "attempt 2/3" in out
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def test_request_clamps_negative_retry_after(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
1067
|
+
# time.sleep raises ValueError on negative values.
|
|
1068
|
+
sleeps: list[float] = []
|
|
1069
|
+
monkeypatch.setattr("time.sleep", lambda s: sleeps.append(s))
|
|
1070
|
+
transport = _MockTransport(
|
|
1071
|
+
_rate_limit_response(retry_after="-5"),
|
|
1072
|
+
_json_response({}),
|
|
1073
|
+
)
|
|
1074
|
+
with httpx.Client(transport=transport) as http:
|
|
1075
|
+
http_request_with_retry(
|
|
1076
|
+
lambda: http.get("https://example.atlassian.net/wiki/api/v2/test"), "test_op"
|
|
1077
|
+
)
|
|
1078
|
+
assert sleeps == [0.0]
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def test_client_get_retries_on_429(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
1082
|
+
# Reads are rate-limited too; a 429 on a lookup must not abort the publish.
|
|
1083
|
+
monkeypatch.setattr("time.sleep", lambda _: None)
|
|
1084
|
+
transport = _MockTransport(
|
|
1085
|
+
_rate_limit_response(),
|
|
1086
|
+
_json_response({"results": [{"id": "7", "title": "Home"}]}),
|
|
1087
|
+
)
|
|
1088
|
+
with ConfluenceClient(_make_config()) as client:
|
|
1089
|
+
client._client = httpx.Client(transport=transport) # type: ignore[assignment]
|
|
1090
|
+
page = client.find_page("42", "Home")
|
|
1091
|
+
assert page == {"id": "7", "title": "Home"}
|
|
1092
|
+
assert len(transport.requests) == 2
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def test_client_paginated_get_retries_on_429(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
1096
|
+
monkeypatch.setattr("time.sleep", lambda _: None)
|
|
1097
|
+
transport = _MockTransport(
|
|
1098
|
+
_json_response({
|
|
1099
|
+
"results": [{"id": "1", "type": "page"}],
|
|
1100
|
+
"_links": {"next": "/wiki/api/v2/pages/9/descendants?cursor=abc"},
|
|
1101
|
+
}),
|
|
1102
|
+
_rate_limit_response(),
|
|
1103
|
+
_json_response({"results": [{"id": "2", "type": "page"}], "_links": {}}),
|
|
1104
|
+
)
|
|
1105
|
+
with ConfluenceClient(_make_config()) as client:
|
|
1106
|
+
client._client = httpx.Client(transport=transport) # type: ignore[assignment]
|
|
1107
|
+
ids = client.get_descendant_ids("9")
|
|
1108
|
+
assert ids == ["1", "2"]
|
|
1109
|
+
assert transport.requests[2].url.params["cursor"] == "abc"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs2confluence.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs2confluence.egg-info/requires.txt
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs2confluence.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/compiler/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/compiler/models.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/compiler/page.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/emitter/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/ir/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/ir/treeutil.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/loader/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/loader/config.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/loader/extra_css.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/loader/nav.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/loader/page.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/parser/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/parser/markdown.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/pdf/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/pdf/generator.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/pdf/render.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/abbrevs.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/fence.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preprocess/icons.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preview/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preview/render.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/preview/server.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/executor.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/models.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/publisher/planner.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/skill_installer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/anchoring.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/command.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/comments.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/github.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/platform.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/sync/state.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/_kroki.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/abbrevs.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/assets.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/footer.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/images.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/src/mkdocs_to_confluence/transforms/mermaid.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.20.2 → mkdocs2confluence-0.20.3}/tests/test_exclude_properties_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|