mkdocs2confluence 0.6.0__tar.gz → 0.6.1__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.6.0/src/mkdocs2confluence.egg-info → mkdocs2confluence-0.6.1}/PKG-INFO +2 -1
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/README.md +1 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/pyproject.toml +9 -2
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1/src/mkdocs2confluence.egg-info}/PKG-INFO +2 -1
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/config.py +1 -1
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/nav.py +1 -3
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/includes.py +1 -1
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/publisher/pipeline.py +185 -140
- mkdocs2confluence-0.6.1/tests/test_images.py +320 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_preprocess.py +11 -1
- mkdocs2confluence-0.6.1/tests/test_preview.py +326 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_publish_client.py +139 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_publish_config.py +152 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_publish_pipeline.py +751 -0
- mkdocs2confluence-0.6.0/tests/test_images.py +0 -160
- mkdocs2confluence-0.6.0/tests/test_preview.py +0 -133
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/LICENSE +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/setup.cfg +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/__init__.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/cli.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/ir/document.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/page.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preview/render.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/publisher/client.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/images.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_abbrevs.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_editlink.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_emitter.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_extra_css.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_frontmatter.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_icons.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_internallinks.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_ir.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_linkdefs.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_loader.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_mermaid.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_page_loader.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_parser.py +0 -0
- {mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/tests/test_treeutil.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs2confluence
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
|
|
5
5
|
Author: Anders Hybertz
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -41,6 +41,7 @@ Dynamic: license-file
|
|
|
41
41
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
42
42
|
[](https://www.python.org/downloads/)
|
|
43
43
|
[](https://pypi.org/project/mkdocs2confluence/)
|
|
44
|
+
[](https://pypi.org/project/mkdocs2confluence/)
|
|
44
45
|
[](https://github.com/jeckyl2010/mkdocs2confluence/releases/latest)
|
|
45
46
|
[](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/ci.yml)
|
|
46
47
|
[](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/release.yml)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
4
4
|
[](https://www.python.org/downloads/)
|
|
5
5
|
[](https://pypi.org/project/mkdocs2confluence/)
|
|
6
|
+
[](https://pypi.org/project/mkdocs2confluence/)
|
|
6
7
|
[](https://github.com/jeckyl2010/mkdocs2confluence/releases/latest)
|
|
7
8
|
[](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/ci.yml)
|
|
8
9
|
[](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/release.yml)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mkdocs2confluence"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.1"
|
|
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" }
|
|
@@ -88,4 +88,11 @@ exclude_lines = [
|
|
|
88
88
|
"pragma: no cover",
|
|
89
89
|
"if TYPE_CHECKING:",
|
|
90
90
|
"raise NotImplementedError",
|
|
91
|
-
]
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
[dependency-groups]
|
|
94
|
+
dev = [
|
|
95
|
+
"pip-audit>=2.10.0",
|
|
96
|
+
"radon>=6.0.1",
|
|
97
|
+
"vulture>=2.16",
|
|
98
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs2confluence
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
|
|
5
5
|
Author: Anders Hybertz
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -41,6 +41,7 @@ Dynamic: license-file
|
|
|
41
41
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
42
42
|
[](https://www.python.org/downloads/)
|
|
43
43
|
[](https://pypi.org/project/mkdocs2confluence/)
|
|
44
|
+
[](https://pypi.org/project/mkdocs2confluence/)
|
|
44
45
|
[](https://github.com/jeckyl2010/mkdocs2confluence/releases/latest)
|
|
45
46
|
[](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/ci.yml)
|
|
46
47
|
[](https://github.com/jeckyl2010/mkdocs2confluence/actions/workflows/release.yml)
|
{mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/loader/config.py
RENAMED
|
@@ -107,7 +107,7 @@ def _make_env_loader() -> type[yaml.SafeLoader]:
|
|
|
107
107
|
|
|
108
108
|
# Catch-all: any other unknown tag (e.g. !!python/name:... used by
|
|
109
109
|
# MkDocs Material) is silently ignored — we only care about nav/site_name.
|
|
110
|
-
def _ignore(loader: yaml.SafeLoader,
|
|
110
|
+
def _ignore(loader: yaml.SafeLoader, _tag_suffix: str, node: yaml.Node) -> None:
|
|
111
111
|
return None
|
|
112
112
|
|
|
113
113
|
_Loader.add_multi_constructor("", _ignore) # type: ignore[no-untyped-call]
|
|
@@ -39,7 +39,7 @@ class NavNode:
|
|
|
39
39
|
return self.docs_path is not None
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def resolve_nav(config: MkDocsConfig
|
|
42
|
+
def resolve_nav(config: MkDocsConfig) -> list[NavNode]:
|
|
43
43
|
"""Resolve *config.nav* into a list of top-level :class:`NavNode` objects.
|
|
44
44
|
|
|
45
45
|
When ``config.nav`` is ``None`` (e.g. projects using ``awesome-pages`` or
|
|
@@ -48,8 +48,6 @@ def resolve_nav(config: MkDocsConfig, mkdocs_root: Path | None = None) -> list[N
|
|
|
48
48
|
|
|
49
49
|
Args:
|
|
50
50
|
config: Parsed :class:`~mkdocs_to_confluence.loader.config.MkDocsConfig`.
|
|
51
|
-
mkdocs_root: Directory containing ``mkdocs.yml``. Used only to compute
|
|
52
|
-
``docs_dir`` when it is not already absolute; defaults to CWD.
|
|
53
51
|
|
|
54
52
|
Returns:
|
|
55
53
|
List of top-level :class:`NavNode` instances.
|
{mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/preprocess/includes.py
RENAMED
|
@@ -290,7 +290,7 @@ def _resolve_include_path(rel_path: str, source_path: Path, docs_dir: Path) -> P
|
|
|
290
290
|
"""
|
|
291
291
|
for base in (docs_dir, source_path.parent):
|
|
292
292
|
candidate = (base / rel_path).resolve()
|
|
293
|
-
if candidate.is_file():
|
|
293
|
+
if candidate.is_file() and candidate.is_relative_to(base.resolve()):
|
|
294
294
|
return candidate
|
|
295
295
|
return None
|
|
296
296
|
|
{mkdocs2confluence-0.6.0 → mkdocs2confluence-0.6.1}/src/mkdocs_to_confluence/publisher/pipeline.py
RENAMED
|
@@ -14,6 +14,7 @@ The pipeline has two phases:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import hashlib
|
|
17
|
+
import re
|
|
17
18
|
from dataclasses import dataclass, field
|
|
18
19
|
from datetime import datetime, timezone
|
|
19
20
|
from pathlib import Path
|
|
@@ -57,7 +58,7 @@ if TYPE_CHECKING:
|
|
|
57
58
|
|
|
58
59
|
_Action = Literal["create", "update", "skip", "section"]
|
|
59
60
|
|
|
60
|
-
_FRONT_MATTER_RE =
|
|
61
|
+
_FRONT_MATTER_RE = re.compile(r"\A---\s*\n(.*?\n?)---\s*\n?", re.DOTALL)
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
@dataclass
|
|
@@ -457,6 +458,184 @@ def _upload_assets(
|
|
|
457
458
|
return uploaded, skipped, errors
|
|
458
459
|
|
|
459
460
|
|
|
461
|
+
def _execute_folder_action(
|
|
462
|
+
action: PageAction,
|
|
463
|
+
client: ConfluenceClient,
|
|
464
|
+
space_id: str,
|
|
465
|
+
root_page_id: str | None,
|
|
466
|
+
report: PublishReport,
|
|
467
|
+
) -> None:
|
|
468
|
+
"""Handle folder create/find for a single folder action."""
|
|
469
|
+
if action.page_id is not None:
|
|
470
|
+
# Already found at plan time — reuse.
|
|
471
|
+
report.updated += 1
|
|
472
|
+
elif action.parent_is_folder or action.parent_id == root_page_id:
|
|
473
|
+
# Parent is a Confluence folder, or this is a top-level section
|
|
474
|
+
# directly under the configured root page — use native folder API.
|
|
475
|
+
existing_folder = None
|
|
476
|
+
if action.parent_id is not None:
|
|
477
|
+
try:
|
|
478
|
+
existing_folder = client.find_folder_under(
|
|
479
|
+
action.parent_id,
|
|
480
|
+
action.title,
|
|
481
|
+
parent_is_folder=action.parent_is_folder,
|
|
482
|
+
)
|
|
483
|
+
except Exception as find_exc:
|
|
484
|
+
print(
|
|
485
|
+
f" [warn] find_folder_under failed "
|
|
486
|
+
f"(parent_id={action.parent_id}): {find_exc}"
|
|
487
|
+
)
|
|
488
|
+
if existing_folder is not None:
|
|
489
|
+
action.page_id = str(existing_folder["id"])
|
|
490
|
+
report.updated += 1
|
|
491
|
+
else:
|
|
492
|
+
folder_parent = (
|
|
493
|
+
action.parent_id if action.parent_is_folder else None
|
|
494
|
+
)
|
|
495
|
+
folder = client.create_folder(
|
|
496
|
+
space_id, action.title, parent_id=folder_parent
|
|
497
|
+
)
|
|
498
|
+
action.page_id = str(folder["id"])
|
|
499
|
+
report.created += 1
|
|
500
|
+
print(
|
|
501
|
+
f" folder id={action.page_id}"
|
|
502
|
+
f" parent_id={action.parent_id}"
|
|
503
|
+
f" parent_is_folder={action.parent_is_folder}"
|
|
504
|
+
)
|
|
505
|
+
else:
|
|
506
|
+
# Parent is a dynamically-created page (e.g. a section-index page).
|
|
507
|
+
# Confluence folders cannot be nested under pages — use a stub page.
|
|
508
|
+
action.is_folder = False
|
|
509
|
+
existing = client.find_page(space_id, action.title)
|
|
510
|
+
if existing is not None:
|
|
511
|
+
action.page_id = str(existing["id"])
|
|
512
|
+
report.updated += 1
|
|
513
|
+
else:
|
|
514
|
+
stub = client.create_page(
|
|
515
|
+
space_id, action.title, "", parent_id=action.parent_id,
|
|
516
|
+
)
|
|
517
|
+
action.page_id = str(stub["id"])
|
|
518
|
+
report.created += 1
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _execute_page_action(
|
|
522
|
+
action: PageAction,
|
|
523
|
+
client: ConfluenceClient,
|
|
524
|
+
space_id: str,
|
|
525
|
+
report: PublishReport,
|
|
526
|
+
) -> None:
|
|
527
|
+
"""Handle create/update (with stale fallback) for a single page action."""
|
|
528
|
+
if action.action == "create":
|
|
529
|
+
page = client.create_page(
|
|
530
|
+
space_id,
|
|
531
|
+
action.title,
|
|
532
|
+
action.xhtml or "",
|
|
533
|
+
parent_id=action.parent_id,
|
|
534
|
+
)
|
|
535
|
+
action.page_id = str(page["id"])
|
|
536
|
+
report.created += 1
|
|
537
|
+
try:
|
|
538
|
+
client.stamp_managed(action.page_id)
|
|
539
|
+
except Exception:
|
|
540
|
+
pass # non-fatal
|
|
541
|
+
elif action.action == "update":
|
|
542
|
+
if action.page_id is None or action.version is None:
|
|
543
|
+
raise RuntimeError(
|
|
544
|
+
f"Update action for '{action.title}' is missing page_id or version"
|
|
545
|
+
)
|
|
546
|
+
try:
|
|
547
|
+
client.update_page(
|
|
548
|
+
action.page_id,
|
|
549
|
+
action.title,
|
|
550
|
+
action.xhtml or "",
|
|
551
|
+
action.version + 1,
|
|
552
|
+
parent_id=action.parent_id,
|
|
553
|
+
)
|
|
554
|
+
report.updated += 1
|
|
555
|
+
except ConfluenceError as upd_exc:
|
|
556
|
+
err = str(upd_exc)
|
|
557
|
+
# 404 = page deleted; 400 "another space" = stale page_id
|
|
558
|
+
# from a different Confluence space. Both mean the existing
|
|
559
|
+
# page can't be updated — fall back to create a fresh one.
|
|
560
|
+
is_stale = "HTTP 404" in err or (
|
|
561
|
+
"HTTP 400" in err and "another space" in err.lower()
|
|
562
|
+
)
|
|
563
|
+
if not is_stale:
|
|
564
|
+
raise
|
|
565
|
+
print(
|
|
566
|
+
f" [warn] update failed ({err[:80].strip()}) —"
|
|
567
|
+
" stale page_id; falling back to create"
|
|
568
|
+
)
|
|
569
|
+
action.page_id = None
|
|
570
|
+
page = client.create_page(
|
|
571
|
+
space_id,
|
|
572
|
+
action.title,
|
|
573
|
+
action.xhtml or "",
|
|
574
|
+
parent_id=action.parent_id,
|
|
575
|
+
)
|
|
576
|
+
action.page_id = str(page["id"])
|
|
577
|
+
report.created += 1
|
|
578
|
+
try:
|
|
579
|
+
client.stamp_managed(action.page_id)
|
|
580
|
+
except Exception:
|
|
581
|
+
pass # non-fatal
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _wire_children(
|
|
585
|
+
action: PageAction,
|
|
586
|
+
action_by_node: dict[int, PageAction],
|
|
587
|
+
) -> None:
|
|
588
|
+
"""Propagate a section's resolved page_id to all its direct children."""
|
|
589
|
+
if action.page_id is None:
|
|
590
|
+
return
|
|
591
|
+
for child_node in action.node.children:
|
|
592
|
+
child_action = action_by_node.get(id(child_node))
|
|
593
|
+
if child_action is not None:
|
|
594
|
+
child_action.parent_id = action.page_id
|
|
595
|
+
child_action.parent_is_folder = action.is_folder
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _post_process_action(
|
|
599
|
+
action: PageAction,
|
|
600
|
+
client: ConfluenceClient,
|
|
601
|
+
*,
|
|
602
|
+
full_width: bool,
|
|
603
|
+
docs_dir: Path,
|
|
604
|
+
report: PublishReport,
|
|
605
|
+
) -> None:
|
|
606
|
+
"""Run all non-fatal post-create/update work for a single action."""
|
|
607
|
+
# Store content hash after create/update so the next run can skip unchanged pages.
|
|
608
|
+
if action.page_id and action.content_hash and action.action in ("create", "update"):
|
|
609
|
+
try:
|
|
610
|
+
client.set_content_hash(action.page_id, action.content_hash)
|
|
611
|
+
except Exception:
|
|
612
|
+
pass # non-fatal
|
|
613
|
+
|
|
614
|
+
# Set full-width layout on newly created or updated pages (not folders).
|
|
615
|
+
if full_width and action.page_id and not action.is_folder:
|
|
616
|
+
try:
|
|
617
|
+
client.set_page_full_width(action.page_id)
|
|
618
|
+
except Exception:
|
|
619
|
+
pass # non-fatal — page is published, layout is cosmetic
|
|
620
|
+
|
|
621
|
+
# Apply labels (tags) from front matter — non-fatal on failure.
|
|
622
|
+
if action.page_id and action.labels and not action.is_folder:
|
|
623
|
+
try:
|
|
624
|
+
client.set_page_labels(action.page_id, action.labels)
|
|
625
|
+
except Exception:
|
|
626
|
+
pass
|
|
627
|
+
|
|
628
|
+
# Upload assets — skip files whose mtime is not newer than Confluence.
|
|
629
|
+
if action.page_id and action.attachments:
|
|
630
|
+
uploaded, asset_skipped, asset_errors = _upload_assets(
|
|
631
|
+
action.page_id, action.attachments, docs_dir, client
|
|
632
|
+
)
|
|
633
|
+
report.assets_uploaded += uploaded
|
|
634
|
+
report.assets_skipped += asset_skipped
|
|
635
|
+
for name, msg in asset_errors:
|
|
636
|
+
report.errors.append((f"{action.title} / {name}", msg))
|
|
637
|
+
|
|
638
|
+
|
|
460
639
|
def execute_publish(
|
|
461
640
|
plan: list[PageAction],
|
|
462
641
|
client: ConfluenceClient,
|
|
@@ -508,108 +687,9 @@ def execute_publish(
|
|
|
508
687
|
|
|
509
688
|
try:
|
|
510
689
|
if action.is_folder:
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
elif action.parent_is_folder or action.parent_id == root_page_id:
|
|
515
|
-
# Parent is a Confluence folder, or this is a top-level section
|
|
516
|
-
# directly under the configured root page — use native folder API.
|
|
517
|
-
existing_folder = None
|
|
518
|
-
if action.parent_id is not None:
|
|
519
|
-
try:
|
|
520
|
-
existing_folder = client.find_folder_under(
|
|
521
|
-
action.parent_id,
|
|
522
|
-
action.title,
|
|
523
|
-
parent_is_folder=action.parent_is_folder,
|
|
524
|
-
)
|
|
525
|
-
except Exception as find_exc:
|
|
526
|
-
print(
|
|
527
|
-
f" [warn] find_folder_under failed "
|
|
528
|
-
f"(parent_id={action.parent_id}): {find_exc}"
|
|
529
|
-
)
|
|
530
|
-
if existing_folder is not None:
|
|
531
|
-
action.page_id = str(existing_folder["id"])
|
|
532
|
-
report.updated += 1
|
|
533
|
-
else:
|
|
534
|
-
folder_parent = (
|
|
535
|
-
action.parent_id if action.parent_is_folder else None
|
|
536
|
-
)
|
|
537
|
-
folder = client.create_folder(
|
|
538
|
-
space_id, action.title, parent_id=folder_parent
|
|
539
|
-
)
|
|
540
|
-
action.page_id = str(folder["id"])
|
|
541
|
-
report.created += 1
|
|
542
|
-
print(
|
|
543
|
-
f" folder id={action.page_id}"
|
|
544
|
-
f" parent_id={action.parent_id}"
|
|
545
|
-
f" parent_is_folder={action.parent_is_folder}"
|
|
546
|
-
)
|
|
547
|
-
else:
|
|
548
|
-
# Parent is a dynamically-created page (e.g. a section-index page).
|
|
549
|
-
# Confluence folders cannot be nested under pages — use a stub page.
|
|
550
|
-
action.is_folder = False
|
|
551
|
-
existing = client.find_page(space_id, action.title)
|
|
552
|
-
if existing is not None:
|
|
553
|
-
action.page_id = str(existing["id"])
|
|
554
|
-
report.updated += 1
|
|
555
|
-
else:
|
|
556
|
-
stub = client.create_page(
|
|
557
|
-
space_id, action.title, "", parent_id=action.parent_id,
|
|
558
|
-
)
|
|
559
|
-
action.page_id = str(stub["id"])
|
|
560
|
-
report.created += 1
|
|
561
|
-
elif action.action == "create":
|
|
562
|
-
page = client.create_page(
|
|
563
|
-
space_id,
|
|
564
|
-
action.title,
|
|
565
|
-
action.xhtml or "",
|
|
566
|
-
parent_id=action.parent_id,
|
|
567
|
-
)
|
|
568
|
-
action.page_id = str(page["id"])
|
|
569
|
-
report.created += 1
|
|
570
|
-
try:
|
|
571
|
-
client.stamp_managed(action.page_id)
|
|
572
|
-
except Exception:
|
|
573
|
-
pass # non-fatal
|
|
574
|
-
elif action.action == "update":
|
|
575
|
-
assert action.page_id is not None
|
|
576
|
-
assert action.version is not None
|
|
577
|
-
try:
|
|
578
|
-
client.update_page(
|
|
579
|
-
action.page_id,
|
|
580
|
-
action.title,
|
|
581
|
-
action.xhtml or "",
|
|
582
|
-
action.version + 1,
|
|
583
|
-
parent_id=action.parent_id,
|
|
584
|
-
)
|
|
585
|
-
report.updated += 1
|
|
586
|
-
except ConfluenceError as upd_exc:
|
|
587
|
-
err = str(upd_exc)
|
|
588
|
-
# 404 = page deleted; 400 "another space" = stale page_id
|
|
589
|
-
# from a different Confluence space. Both mean the existing
|
|
590
|
-
# page can't be updated — fall back to create a fresh one.
|
|
591
|
-
is_stale = "HTTP 404" in err or (
|
|
592
|
-
"HTTP 400" in err and "another space" in err.lower()
|
|
593
|
-
)
|
|
594
|
-
if not is_stale:
|
|
595
|
-
raise
|
|
596
|
-
print(
|
|
597
|
-
f" [warn] update failed ({err[:80].strip()}) —"
|
|
598
|
-
" stale page_id; falling back to create"
|
|
599
|
-
)
|
|
600
|
-
action.page_id = None
|
|
601
|
-
page = client.create_page(
|
|
602
|
-
space_id,
|
|
603
|
-
action.title,
|
|
604
|
-
action.xhtml or "",
|
|
605
|
-
parent_id=action.parent_id,
|
|
606
|
-
)
|
|
607
|
-
action.page_id = str(page["id"])
|
|
608
|
-
report.created += 1
|
|
609
|
-
try:
|
|
610
|
-
client.stamp_managed(action.page_id)
|
|
611
|
-
except Exception:
|
|
612
|
-
pass # non-fatal
|
|
690
|
+
_execute_folder_action(action, client, space_id, root_page_id, report)
|
|
691
|
+
else:
|
|
692
|
+
_execute_page_action(action, client, space_id, report)
|
|
613
693
|
except Exception as exc:
|
|
614
694
|
report.errors.append((action.title, str(exc)))
|
|
615
695
|
# Do NOT `continue` — all post-execute blocks below are guarded by
|
|
@@ -617,45 +697,10 @@ def execute_publish(
|
|
|
617
697
|
# Critically, child parent_id wiring must still run for section
|
|
618
698
|
# pages whose children were planned with parent_id=None.
|
|
619
699
|
|
|
620
|
-
# Once a folder/section's page_id is known, wire it into all direct
|
|
621
|
-
# children so that children created later use the correct parent_id.
|
|
622
700
|
if action.node.is_section and action.page_id:
|
|
623
|
-
|
|
624
|
-
child_action = action_by_node.get(id(child_node))
|
|
625
|
-
if child_action is not None:
|
|
626
|
-
child_action.parent_id = action.page_id
|
|
627
|
-
child_action.parent_is_folder = action.is_folder
|
|
628
|
-
|
|
629
|
-
# Store content hash after create/update so the next run can skip unchanged pages.
|
|
630
|
-
if action.page_id and action.content_hash and action.action in ("create", "update"):
|
|
631
|
-
try:
|
|
632
|
-
client.set_content_hash(action.page_id, action.content_hash)
|
|
633
|
-
except Exception:
|
|
634
|
-
pass # non-fatal
|
|
635
|
-
|
|
636
|
-
# Set full-width layout on newly created or updated pages (not folders).
|
|
637
|
-
if full_width and action.page_id and not action.is_folder:
|
|
638
|
-
try:
|
|
639
|
-
client.set_page_full_width(action.page_id)
|
|
640
|
-
except Exception:
|
|
641
|
-
pass # non-fatal — page is published, layout is cosmetic
|
|
701
|
+
_wire_children(action, action_by_node)
|
|
642
702
|
|
|
643
|
-
|
|
644
|
-
if action.page_id and action.labels and not action.is_folder:
|
|
645
|
-
try:
|
|
646
|
-
client.set_page_labels(action.page_id, action.labels)
|
|
647
|
-
except Exception:
|
|
648
|
-
pass
|
|
649
|
-
|
|
650
|
-
# Upload assets — skip files whose mtime is not newer than Confluence.
|
|
651
|
-
if action.page_id and action.attachments:
|
|
652
|
-
uploaded, asset_skipped, asset_errors = _upload_assets(
|
|
653
|
-
action.page_id, action.attachments, docs_dir, client
|
|
654
|
-
)
|
|
655
|
-
report.assets_uploaded += uploaded
|
|
656
|
-
report.assets_skipped += asset_skipped
|
|
657
|
-
for name, msg in asset_errors:
|
|
658
|
-
report.errors.append((f"{action.title} / {name}", msg))
|
|
703
|
+
_post_process_action(action, client, full_width=full_width, docs_dir=docs_dir, report=report)
|
|
659
704
|
|
|
660
705
|
if prune and root_page_id:
|
|
661
706
|
published_ids = {a.page_id for a in plan if a.page_id}
|