confpub-cli 1.7.4__tar.gz → 1.7.5__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.
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/PKG-INFO +1 -1
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/__init__.py +1 -1
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/front_matter.py +18 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/puller.py +27 -50
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_puller.py +14 -10
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/.github/copilot-instructions.md +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/.github/workflows/publish.yml +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/.gitignore +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/CLAUDE.md +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/LICENSE +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/PRD.md +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/README.md +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/applier.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/assets.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/cli.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/config.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/confluence.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/converter.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/envelope.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/errors.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/guide.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/lockfile.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/macro_plugin.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/manifest.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/output.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/planner.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/publish.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/py.typed +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/reverse_converter.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/validator.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub/verifier.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/confpub.lock +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/pyproject.toml +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/__init__.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/conftest.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_applier.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_assets.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_config.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_confluence.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_converter.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_envelope.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_errors.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_front_matter.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_guide.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_integration.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_lockfile.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_macro_plugin.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_manifest.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_output.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_planner.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_publish.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_reverse_converter.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_validator.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/tests/test_verifier.py +0 -0
- {confpub_cli-1.7.4 → confpub_cli-1.7.5}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: confpub-cli
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.5
|
|
4
4
|
Summary: Agent-first CLI to publish Markdown to Confluence
|
|
5
5
|
Project-URL: Homepage, https://github.com/ThomasRohde/confpub-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/ThomasRohde/confpub-cli.git
|
|
@@ -10,6 +10,8 @@ import dataclasses
|
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
13
15
|
from confpub.converter import extract_front_matter
|
|
14
16
|
from confpub.errors import ERR_VALIDATION_MARKDOWN, ConfpubError
|
|
15
17
|
|
|
@@ -24,6 +26,22 @@ class FrontMatterData:
|
|
|
24
26
|
labels: list[str] = field(default_factory=list)
|
|
25
27
|
page_id: str | None = None
|
|
26
28
|
|
|
29
|
+
def to_yaml_block(self) -> str:
|
|
30
|
+
"""Serialize to a YAML front matter block (``--- ... ---``)."""
|
|
31
|
+
data: dict[str, Any] = {}
|
|
32
|
+
if self.title is not None:
|
|
33
|
+
data["title"] = self.title
|
|
34
|
+
if self.page_id is not None:
|
|
35
|
+
data["page_id"] = self.page_id
|
|
36
|
+
if self.space is not None:
|
|
37
|
+
data["space"] = self.space
|
|
38
|
+
if self.parent is not None:
|
|
39
|
+
data["parent"] = self.parent
|
|
40
|
+
if self.labels:
|
|
41
|
+
data["labels"] = self.labels
|
|
42
|
+
yaml_str = yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
43
|
+
return f"---\n{yaml_str}---\n\n"
|
|
44
|
+
|
|
27
45
|
|
|
28
46
|
def _validate_string(raw: dict[str, Any], key: str) -> str | None:
|
|
29
47
|
"""Extract and validate a string field, or None if absent."""
|
|
@@ -9,9 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import os
|
|
10
10
|
import re
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
13
|
-
|
|
14
|
-
import yaml
|
|
12
|
+
from typing import Any, Literal
|
|
15
13
|
|
|
16
14
|
from confpub.confluence import ConfluenceClient, build_client
|
|
17
15
|
from confpub.output import emit_progress
|
|
@@ -21,10 +19,13 @@ from confpub.errors import (
|
|
|
21
19
|
ERR_VALIDATION_REQUIRED,
|
|
22
20
|
ConfpubError,
|
|
23
21
|
)
|
|
22
|
+
from confpub.front_matter import FrontMatterData
|
|
24
23
|
from confpub.lockfile import Lockfile, load_lockfile, save_lockfile, update_lockfile
|
|
25
24
|
from confpub.manifest import generate_manifest_yaml
|
|
26
25
|
from confpub.reverse_converter import convert_storage_to_markdown
|
|
27
26
|
|
|
27
|
+
Layout = Literal["flat", "nested"]
|
|
28
|
+
|
|
28
29
|
|
|
29
30
|
def _slugify(title: str) -> str:
|
|
30
31
|
"""Convert a page title to a filename-safe slug.
|
|
@@ -77,15 +78,22 @@ def _collect_tree(
|
|
|
77
78
|
def _compute_file_paths(
|
|
78
79
|
pages: list[dict[str, Any]],
|
|
79
80
|
output_dir: str,
|
|
80
|
-
layout:
|
|
81
|
+
layout: Layout,
|
|
81
82
|
root_page_id: str,
|
|
82
|
-
) -> dict[str, str]:
|
|
83
|
-
"""Compute output file paths for each page.
|
|
83
|
+
) -> tuple[dict[str, str], dict[str, str], dict[str, str | None]]:
|
|
84
|
+
"""Compute output file paths and lookup maps for each page.
|
|
85
|
+
|
|
86
|
+
Returns (file_paths, id_to_title, id_to_parent) where:
|
|
87
|
+
- file_paths: {page_id: file_path}
|
|
88
|
+
- id_to_title: {page_id: title}
|
|
89
|
+
- id_to_parent: {page_id: parent_page_id or None}
|
|
90
|
+
"""
|
|
84
91
|
paths: dict[str, str] = {}
|
|
85
92
|
root_slug: str | None = None
|
|
86
93
|
|
|
87
|
-
# Build
|
|
94
|
+
# Build lookup maps
|
|
88
95
|
id_to_slug: dict[str, str] = {}
|
|
96
|
+
id_to_title: dict[str, str] = {}
|
|
89
97
|
id_to_parent: dict[str, str | None] = {}
|
|
90
98
|
|
|
91
99
|
for entry in pages:
|
|
@@ -93,6 +101,7 @@ def _compute_file_paths(
|
|
|
93
101
|
pid = str(page["id"])
|
|
94
102
|
slug = _slugify(page.get("title", pid))
|
|
95
103
|
id_to_slug[pid] = slug
|
|
104
|
+
id_to_title[pid] = page.get("title", "")
|
|
96
105
|
id_to_parent[pid] = str(entry["parent_id"]) if entry["parent_id"] else None
|
|
97
106
|
if pid == root_page_id:
|
|
98
107
|
root_slug = slug
|
|
@@ -119,7 +128,7 @@ def _compute_file_paths(
|
|
|
119
128
|
# Flat layout
|
|
120
129
|
paths[pid] = os.path.join(output_dir, f"{slug}.md")
|
|
121
130
|
|
|
122
|
-
return paths
|
|
131
|
+
return paths, id_to_title, id_to_parent
|
|
123
132
|
|
|
124
133
|
|
|
125
134
|
def _check_conflicts(file_paths: dict[str, str], force: bool) -> None:
|
|
@@ -147,7 +156,7 @@ def _download_page_attachments(
|
|
|
147
156
|
page_id: str,
|
|
148
157
|
slug: str,
|
|
149
158
|
output_dir: str,
|
|
150
|
-
layout:
|
|
159
|
+
layout: Layout,
|
|
151
160
|
warnings: list[str],
|
|
152
161
|
file_path: str | None = None,
|
|
153
162
|
) -> dict[str, str]:
|
|
@@ -164,8 +173,6 @@ def _download_page_attachments(
|
|
|
164
173
|
if layout == "nested" and file_path:
|
|
165
174
|
# Place assets next to the markdown file (e.g. .../page-slug/assets/)
|
|
166
175
|
assets_dir = os.path.join(os.path.dirname(file_path), "assets")
|
|
167
|
-
elif layout == "nested":
|
|
168
|
-
assets_dir = os.path.join(output_dir, slug, "assets")
|
|
169
176
|
else:
|
|
170
177
|
assets_dir = os.path.join(output_dir, "assets", slug)
|
|
171
178
|
|
|
@@ -234,27 +241,6 @@ def _build_page_tree(
|
|
|
234
241
|
return [root_entry]
|
|
235
242
|
|
|
236
243
|
|
|
237
|
-
def _build_front_matter(
|
|
238
|
-
title: str,
|
|
239
|
-
page_id: str,
|
|
240
|
-
space: str,
|
|
241
|
-
parent: str | None = None,
|
|
242
|
-
labels: list[str] | None = None,
|
|
243
|
-
) -> str:
|
|
244
|
-
"""Build a YAML front matter block for a pulled markdown file."""
|
|
245
|
-
data: dict[str, Any] = {
|
|
246
|
-
"title": title,
|
|
247
|
-
"page_id": page_id,
|
|
248
|
-
"space": space,
|
|
249
|
-
}
|
|
250
|
-
if parent:
|
|
251
|
-
data["parent"] = parent
|
|
252
|
-
if labels:
|
|
253
|
-
data["labels"] = labels
|
|
254
|
-
yaml_str = yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
255
|
-
return f"---\n{yaml_str}---\n\n"
|
|
256
|
-
|
|
257
|
-
|
|
258
244
|
def pull_pages(
|
|
259
245
|
*,
|
|
260
246
|
space: str | None = None,
|
|
@@ -263,7 +249,7 @@ def pull_pages(
|
|
|
263
249
|
output_dir: str = ".",
|
|
264
250
|
recursive: bool = False,
|
|
265
251
|
force: bool = False,
|
|
266
|
-
layout:
|
|
252
|
+
layout: Layout = "flat",
|
|
267
253
|
include_attachments: bool = True,
|
|
268
254
|
) -> dict[str, Any]:
|
|
269
255
|
"""Pull pages from Confluence to local Markdown files.
|
|
@@ -296,23 +282,14 @@ def pull_pages(
|
|
|
296
282
|
# Collect all pages to pull
|
|
297
283
|
all_pages = _collect_tree(client, root_id, recursive)
|
|
298
284
|
|
|
299
|
-
# Compute output file paths
|
|
300
|
-
file_paths = _compute_file_paths(all_pages, output_dir, layout, root_id)
|
|
285
|
+
# Compute output file paths and lookup maps
|
|
286
|
+
file_paths, id_to_title, id_to_parent = _compute_file_paths(all_pages, output_dir, layout, root_id)
|
|
301
287
|
|
|
302
288
|
# Check for conflicts
|
|
303
289
|
_check_conflicts(file_paths, force)
|
|
304
290
|
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
id_to_parent: dict[str, str | None] = {}
|
|
308
|
-
for entry in all_pages:
|
|
309
|
-
page = entry["page"]
|
|
310
|
-
pid = str(page["id"])
|
|
311
|
-
id_to_title[pid] = page.get("title", "")
|
|
312
|
-
id_to_parent[pid] = str(entry["parent_id"]) if entry["parent_id"] else None
|
|
313
|
-
|
|
314
|
-
# Get the root page's parent in Confluence (for front matter + manifest)
|
|
315
|
-
ancestors = client.get_page_ancestors(root_id)
|
|
291
|
+
# Get the root page's parent from already-fetched ancestors (no extra API call)
|
|
292
|
+
ancestors = root_page.get("ancestors", [])
|
|
316
293
|
root_parent_title = ancestors[-1].get("title", "") if ancestors else None
|
|
317
294
|
|
|
318
295
|
# Process each page
|
|
@@ -360,14 +337,14 @@ def pull_pages(
|
|
|
360
337
|
parent_title = id_to_title.get(par_id) if par_id else None
|
|
361
338
|
|
|
362
339
|
# Build and prepend front matter
|
|
363
|
-
|
|
340
|
+
fm = FrontMatterData(
|
|
364
341
|
title=page_title,
|
|
365
342
|
page_id=pid,
|
|
366
343
|
space=root_space,
|
|
367
344
|
parent=parent_title,
|
|
368
|
-
labels=page_labels,
|
|
345
|
+
labels=page_labels or [],
|
|
369
346
|
)
|
|
370
|
-
markdown_content =
|
|
347
|
+
markdown_content = fm.to_yaml_block() + conv_result.markdown
|
|
371
348
|
|
|
372
349
|
# Write markdown file
|
|
373
350
|
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
|
@@ -384,7 +361,7 @@ def pull_pages(
|
|
|
384
361
|
|
|
385
362
|
# Always generate manifest
|
|
386
363
|
root_title = root_page.get("title", "")
|
|
387
|
-
manifest_parent =
|
|
364
|
+
manifest_parent = root_parent_title or root_title
|
|
388
365
|
pulled_labels: dict[str, list[str]] = {
|
|
389
366
|
f["page_id"]: f.get("labels", []) for f in files_result
|
|
390
367
|
}
|
|
@@ -10,7 +10,8 @@ import pytest
|
|
|
10
10
|
import yaml
|
|
11
11
|
|
|
12
12
|
from confpub.errors import ERR_CONFLICT_FILE_EXISTS, ERR_VALIDATION_REQUIRED, ConfpubError
|
|
13
|
-
from confpub.
|
|
13
|
+
from confpub.front_matter import FrontMatterData
|
|
14
|
+
from confpub.puller import _slugify, pull_pages
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
# ---------------------------------------------------------------------------
|
|
@@ -594,10 +595,10 @@ class TestManifestFlag:
|
|
|
594
595
|
|
|
595
596
|
class TestBuildFrontMatter:
|
|
596
597
|
def test_basic_fields(self):
|
|
597
|
-
|
|
598
|
-
assert
|
|
599
|
-
assert
|
|
600
|
-
parsed = yaml.safe_load(
|
|
598
|
+
block = FrontMatterData(title="My Page", page_id="123", space="SD").to_yaml_block()
|
|
599
|
+
assert block.startswith("---\n")
|
|
600
|
+
assert block.endswith("---\n\n")
|
|
601
|
+
parsed = yaml.safe_load(block.strip("- \n"))
|
|
601
602
|
assert parsed["title"] == "My Page"
|
|
602
603
|
assert parsed["page_id"] == "123"
|
|
603
604
|
assert parsed["space"] == "SD"
|
|
@@ -605,14 +606,17 @@ class TestBuildFrontMatter:
|
|
|
605
606
|
assert "labels" not in parsed
|
|
606
607
|
|
|
607
608
|
def test_with_parent_and_labels(self):
|
|
608
|
-
|
|
609
|
-
|
|
609
|
+
block = FrontMatterData(
|
|
610
|
+
title="Child", page_id="456", space="SD",
|
|
611
|
+
parent="Parent Page", labels=["a", "b"],
|
|
612
|
+
).to_yaml_block()
|
|
613
|
+
parsed = yaml.safe_load(block.strip("- \n"))
|
|
610
614
|
assert parsed["parent"] == "Parent Page"
|
|
611
615
|
assert parsed["labels"] == ["a", "b"]
|
|
612
616
|
|
|
613
617
|
def test_empty_labels_omitted(self):
|
|
614
|
-
|
|
615
|
-
parsed = yaml.safe_load(
|
|
618
|
+
block = FrontMatterData(title="Page", page_id="1", space="SD").to_yaml_block()
|
|
619
|
+
parsed = yaml.safe_load(block.strip("- \n"))
|
|
616
620
|
assert "labels" not in parsed
|
|
617
621
|
|
|
618
622
|
|
|
@@ -661,8 +665,8 @@ class TestFrontMatterInPulledFiles:
|
|
|
661
665
|
def test_root_page_has_parent_from_ancestors(self, tmp_path):
|
|
662
666
|
"""Root page gets parent from Confluence ancestors."""
|
|
663
667
|
page = _make_page("1", "Root")
|
|
668
|
+
page["ancestors"] = [{"title": "Space Home"}]
|
|
664
669
|
client = _mock_client({"1": page})
|
|
665
|
-
client.get_page_ancestors = lambda pid: [{"title": "Space Home"}]
|
|
666
670
|
|
|
667
671
|
with patch("confpub.puller.build_client", return_value=client):
|
|
668
672
|
pull_pages(page_id="1", output_dir=str(tmp_path))
|
|
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
|
|
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
|