confpub-cli 1.7.2__tar.gz → 1.7.4__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.2 → confpub_cli-1.7.4}/PKG-INFO +1 -1
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/__init__.py +1 -1
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/cli.py +0 -2
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/manifest.py +3 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/puller.py +84 -25
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_puller.py +106 -15
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/.github/copilot-instructions.md +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/.github/workflows/publish.yml +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/.gitignore +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/CLAUDE.md +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/LICENSE +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/PRD.md +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/README.md +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/applier.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/assets.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/config.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/confluence.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/converter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/envelope.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/errors.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/front_matter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/guide.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/lockfile.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/macro_plugin.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/output.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/planner.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/publish.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/py.typed +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/reverse_converter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/validator.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub/verifier.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/confpub.lock +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/pyproject.toml +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/__init__.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/conftest.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_applier.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_assets.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_config.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_confluence.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_converter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_envelope.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_errors.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_front_matter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_guide.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_integration.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_lockfile.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_macro_plugin.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_manifest.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_output.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_planner.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_publish.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_reverse_converter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_validator.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/tests/test_verifier.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.4}/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.4
|
|
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
|
|
@@ -357,7 +357,6 @@ def page_pull(
|
|
|
357
357
|
force: bool = typer.Option(False, "--force", help="Overwrite existing files"),
|
|
358
358
|
layout: str = typer.Option("flat", "--layout", help="Output layout: flat or nested"),
|
|
359
359
|
no_attachments: bool = typer.Option(False, "--no-attachments", help="Skip downloading attachments"),
|
|
360
|
-
manifest: bool = typer.Option(False, "--manifest", help="Generate confpub.yaml manifest"),
|
|
361
360
|
) -> None:
|
|
362
361
|
"""Pull Confluence pages to local Markdown files."""
|
|
363
362
|
with command_context("page.pull", target={"space": space, "title": title, "page_id": page_id}) as ctx:
|
|
@@ -378,7 +377,6 @@ def page_pull(
|
|
|
378
377
|
force=force,
|
|
379
378
|
layout=layout,
|
|
380
379
|
include_attachments=not no_attachments,
|
|
381
|
-
generate_manifest=manifest,
|
|
382
380
|
)
|
|
383
381
|
ctx.warnings.extend(result.pop("warnings", []))
|
|
384
382
|
ctx.result = result
|
|
@@ -191,6 +191,9 @@ def generate_manifest_yaml(
|
|
|
191
191
|
result = []
|
|
192
192
|
for p in pages:
|
|
193
193
|
entry: dict[str, Any] = {"title": p["title"], "file": p["file"]}
|
|
194
|
+
assets = p.get("assets", [])
|
|
195
|
+
if assets:
|
|
196
|
+
entry["assets"] = assets
|
|
194
197
|
labels = p.get("labels", [])
|
|
195
198
|
if labels:
|
|
196
199
|
entry["labels"] = labels
|
|
@@ -11,6 +11,8 @@ import re
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
14
16
|
from confpub.confluence import ConfluenceClient, build_client
|
|
15
17
|
from confpub.output import emit_progress
|
|
16
18
|
from confpub.errors import (
|
|
@@ -147,6 +149,7 @@ def _download_page_attachments(
|
|
|
147
149
|
output_dir: str,
|
|
148
150
|
layout: str,
|
|
149
151
|
warnings: list[str],
|
|
152
|
+
file_path: str | None = None,
|
|
150
153
|
) -> dict[str, str]:
|
|
151
154
|
"""Download attachments for a page. Returns {attachment_name: local_path}.
|
|
152
155
|
|
|
@@ -158,7 +161,10 @@ def _download_page_attachments(
|
|
|
158
161
|
if not attachments:
|
|
159
162
|
return attachment_map
|
|
160
163
|
|
|
161
|
-
if layout == "nested":
|
|
164
|
+
if layout == "nested" and file_path:
|
|
165
|
+
# Place assets next to the markdown file (e.g. .../page-slug/assets/)
|
|
166
|
+
assets_dir = os.path.join(os.path.dirname(file_path), "assets")
|
|
167
|
+
elif layout == "nested":
|
|
162
168
|
assets_dir = os.path.join(output_dir, slug, "assets")
|
|
163
169
|
else:
|
|
164
170
|
assets_dir = os.path.join(output_dir, "assets", slug)
|
|
@@ -188,11 +194,13 @@ def _build_page_tree(
|
|
|
188
194
|
root_page_id: str,
|
|
189
195
|
output_dir: str = ".",
|
|
190
196
|
page_labels: dict[str, list[str]] | None = None,
|
|
197
|
+
page_assets: dict[str, list[str]] | None = None,
|
|
191
198
|
) -> list[dict[str, Any]]:
|
|
192
199
|
"""Build a hierarchical page tree for manifest generation."""
|
|
193
200
|
id_to_entry: dict[str, dict[str, Any]] = {}
|
|
194
201
|
children_map: dict[str | None, list[str]] = {}
|
|
195
202
|
labels_map = page_labels or {}
|
|
203
|
+
assets_map = page_assets or {}
|
|
196
204
|
|
|
197
205
|
for entry in pages:
|
|
198
206
|
page = entry["page"]
|
|
@@ -208,6 +216,8 @@ def _build_page_tree(
|
|
|
208
216
|
}
|
|
209
217
|
if labels_map.get(pid):
|
|
210
218
|
node["labels"] = labels_map[pid]
|
|
219
|
+
if assets_map.get(pid):
|
|
220
|
+
node["assets"] = assets_map[pid]
|
|
211
221
|
id_to_entry[pid] = node
|
|
212
222
|
children_map.setdefault(parent_id, []).append(pid)
|
|
213
223
|
|
|
@@ -224,6 +234,27 @@ def _build_page_tree(
|
|
|
224
234
|
return [root_entry]
|
|
225
235
|
|
|
226
236
|
|
|
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
|
+
|
|
227
258
|
def pull_pages(
|
|
228
259
|
*,
|
|
229
260
|
space: str | None = None,
|
|
@@ -234,7 +265,6 @@ def pull_pages(
|
|
|
234
265
|
force: bool = False,
|
|
235
266
|
layout: str = "flat",
|
|
236
267
|
include_attachments: bool = True,
|
|
237
|
-
generate_manifest: bool = False,
|
|
238
268
|
) -> dict[str, Any]:
|
|
239
269
|
"""Pull pages from Confluence to local Markdown files.
|
|
240
270
|
|
|
@@ -272,10 +302,24 @@ def pull_pages(
|
|
|
272
302
|
# Check for conflicts
|
|
273
303
|
_check_conflicts(file_paths, force)
|
|
274
304
|
|
|
305
|
+
# Build lookup maps for front matter parent resolution
|
|
306
|
+
id_to_title: dict[str, str] = {}
|
|
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)
|
|
316
|
+
root_parent_title = ancestors[-1].get("title", "") if ancestors else None
|
|
317
|
+
|
|
275
318
|
# Process each page
|
|
276
319
|
files_result: list[dict[str, Any]] = []
|
|
277
320
|
total_attachments = 0
|
|
278
321
|
pull_warnings: list[str] = []
|
|
322
|
+
pulled_assets: dict[str, list[str]] = {} # page_id -> list of relative asset paths
|
|
279
323
|
|
|
280
324
|
for entry in all_pages:
|
|
281
325
|
page = entry["page"]
|
|
@@ -288,27 +332,47 @@ def pull_pages(
|
|
|
288
332
|
# Download attachments
|
|
289
333
|
attachment_map: dict[str, str] = {}
|
|
290
334
|
attachments_downloaded = 0
|
|
335
|
+
out_path = file_paths[pid]
|
|
291
336
|
if include_attachments:
|
|
292
337
|
attachment_map = _download_page_attachments(
|
|
293
338
|
client, pid, slug, output_dir, layout, pull_warnings,
|
|
339
|
+
file_path=out_path,
|
|
294
340
|
)
|
|
295
341
|
attachments_downloaded = len(attachment_map)
|
|
296
342
|
total_attachments += attachments_downloaded
|
|
343
|
+
if attachment_map:
|
|
344
|
+
pulled_assets[pid] = list(attachment_map.values())
|
|
297
345
|
|
|
298
346
|
# Convert storage format to markdown
|
|
299
347
|
body_storage = page.get("body", {}).get("storage", {}).get("value", "")
|
|
300
|
-
|
|
348
|
+
conv_result = convert_storage_to_markdown(
|
|
301
349
|
body_storage, attachment_map=attachment_map,
|
|
302
350
|
)
|
|
303
351
|
|
|
304
|
-
# Write markdown file
|
|
305
|
-
out_path = file_paths[pid]
|
|
306
|
-
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
|
307
|
-
Path(out_path).write_text(result.markdown, encoding="utf-8")
|
|
308
|
-
|
|
309
352
|
# Fetch labels
|
|
310
353
|
page_labels = [lbl["name"] for lbl in client.get_labels(pid)]
|
|
311
354
|
|
|
355
|
+
# Determine parent title for front matter
|
|
356
|
+
if pid == root_id:
|
|
357
|
+
parent_title = root_parent_title
|
|
358
|
+
else:
|
|
359
|
+
par_id = id_to_parent.get(pid)
|
|
360
|
+
parent_title = id_to_title.get(par_id) if par_id else None
|
|
361
|
+
|
|
362
|
+
# Build and prepend front matter
|
|
363
|
+
front_matter = _build_front_matter(
|
|
364
|
+
title=page_title,
|
|
365
|
+
page_id=pid,
|
|
366
|
+
space=root_space,
|
|
367
|
+
parent=parent_title,
|
|
368
|
+
labels=page_labels,
|
|
369
|
+
)
|
|
370
|
+
markdown_content = front_matter + conv_result.markdown
|
|
371
|
+
|
|
372
|
+
# Write markdown file
|
|
373
|
+
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
|
374
|
+
Path(out_path).write_text(markdown_content, encoding="utf-8")
|
|
375
|
+
|
|
312
376
|
files_result.append({
|
|
313
377
|
"page_id": pid,
|
|
314
378
|
"title": page_title,
|
|
@@ -318,22 +382,17 @@ def pull_pages(
|
|
|
318
382
|
"labels": page_labels,
|
|
319
383
|
})
|
|
320
384
|
|
|
321
|
-
#
|
|
322
|
-
|
|
323
|
-
if
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
page_tree = _build_page_tree(all_pages, file_paths, root_id, output_dir, page_labels=pulled_labels)
|
|
333
|
-
manifest_yaml = generate_manifest_yaml(root_space, manifest_parent, page_tree)
|
|
334
|
-
manifest_path = os.path.join(output_dir, "confpub.yaml")
|
|
335
|
-
Path(manifest_path).write_text(manifest_yaml, encoding="utf-8")
|
|
336
|
-
manifest_file = manifest_path
|
|
385
|
+
# Always generate manifest
|
|
386
|
+
root_title = root_page.get("title", "")
|
|
387
|
+
manifest_parent = ancestors[-1].get("title", root_title) if ancestors else root_title
|
|
388
|
+
pulled_labels: dict[str, list[str]] = {
|
|
389
|
+
f["page_id"]: f.get("labels", []) for f in files_result
|
|
390
|
+
}
|
|
391
|
+
page_tree = _build_page_tree(all_pages, file_paths, root_id, output_dir, page_labels=pulled_labels, page_assets=pulled_assets)
|
|
392
|
+
manifest_yaml = generate_manifest_yaml(root_space, manifest_parent, page_tree)
|
|
393
|
+
manifest_path = os.path.join(output_dir, "confpub.yaml")
|
|
394
|
+
Path(manifest_path).write_text(manifest_yaml, encoding="utf-8")
|
|
395
|
+
manifest_file: str = manifest_path
|
|
337
396
|
|
|
338
397
|
# Update lockfile
|
|
339
398
|
lockfile_path = os.path.join(output_dir, "confpub.lock")
|
|
@@ -356,6 +415,6 @@ def pull_pages(
|
|
|
356
415
|
"summary": {
|
|
357
416
|
"pages_pulled": len(files_result),
|
|
358
417
|
"attachments_downloaded": total_attachments,
|
|
359
|
-
"manifest_generated":
|
|
418
|
+
"manifest_generated": True,
|
|
360
419
|
},
|
|
361
420
|
}
|
|
@@ -7,8 +7,10 @@ from unittest.mock import MagicMock, patch
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
10
12
|
from confpub.errors import ERR_CONFLICT_FILE_EXISTS, ERR_VALIDATION_REQUIRED, ConfpubError
|
|
11
|
-
from confpub.puller import _slugify, pull_pages
|
|
13
|
+
from confpub.puller import _build_front_matter, _slugify, pull_pages
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
# ---------------------------------------------------------------------------
|
|
@@ -61,12 +63,16 @@ def _mock_client(pages: dict[str, dict], children: dict[str, list] | None = None
|
|
|
61
63
|
def get_page_ancestors(pid):
|
|
62
64
|
return []
|
|
63
65
|
|
|
66
|
+
def get_labels(pid):
|
|
67
|
+
return []
|
|
68
|
+
|
|
64
69
|
client.get_page_by_id = get_page_by_id
|
|
65
70
|
client.get_page = get_page
|
|
66
71
|
client.get_page_children_deep = get_page_children_deep
|
|
67
72
|
client.get_attachments = get_attachments
|
|
68
73
|
client.download_attachment = download_attachment
|
|
69
74
|
client.get_page_ancestors = get_page_ancestors
|
|
75
|
+
client.get_labels = get_labels
|
|
70
76
|
return client
|
|
71
77
|
|
|
72
78
|
|
|
@@ -113,11 +119,14 @@ class TestSinglePagePull:
|
|
|
113
119
|
assert result["files"][0]["page_id"] == "123"
|
|
114
120
|
assert result["files"][0]["title"] == "Overview"
|
|
115
121
|
|
|
116
|
-
# File should exist
|
|
122
|
+
# File should exist with front matter
|
|
117
123
|
md_file = tmp_path / "overview.md"
|
|
118
124
|
assert md_file.exists()
|
|
119
125
|
content = md_file.read_text()
|
|
126
|
+
assert content.startswith("---\n")
|
|
120
127
|
assert "Hello world" in content
|
|
128
|
+
assert "page_id: '123'" in content
|
|
129
|
+
assert "title: Overview" in content
|
|
121
130
|
|
|
122
131
|
def test_pull_single_page_by_space_title(self, tmp_path):
|
|
123
132
|
page = _make_page("456", "Getting Started", "<h1>Welcome</h1>")
|
|
@@ -180,7 +189,6 @@ class TestRecursivePull:
|
|
|
180
189
|
page_id="1",
|
|
181
190
|
output_dir=str(tmp_path),
|
|
182
191
|
recursive=True,
|
|
183
|
-
generate_manifest=True,
|
|
184
192
|
)
|
|
185
193
|
|
|
186
194
|
assert result["summary"]["manifest_generated"] is True
|
|
@@ -410,7 +418,6 @@ class TestDataCenterCompat:
|
|
|
410
418
|
result = pull_pages(
|
|
411
419
|
space="PROJ", title="Root",
|
|
412
420
|
output_dir=str(tmp_path), recursive=True,
|
|
413
|
-
generate_manifest=True,
|
|
414
421
|
)
|
|
415
422
|
|
|
416
423
|
assert result["summary"]["pages_pulled"] == 2
|
|
@@ -482,6 +489,7 @@ class TestMultiLevelRecursivePull:
|
|
|
482
489
|
assert (tmp_path / "child.md").exists()
|
|
483
490
|
assert (tmp_path / "grandchild.md").exists()
|
|
484
491
|
gc_content = (tmp_path / "grandchild.md").read_text()
|
|
492
|
+
assert gc_content.startswith("---\n")
|
|
485
493
|
assert "Deep content" in gc_content
|
|
486
494
|
|
|
487
495
|
def test_page_id_recursive_combination(self, tmp_path):
|
|
@@ -527,7 +535,6 @@ class TestManifestPathSeparators:
|
|
|
527
535
|
output_dir=str(tmp_path),
|
|
528
536
|
recursive=True,
|
|
529
537
|
layout="nested",
|
|
530
|
-
generate_manifest=True,
|
|
531
538
|
)
|
|
532
539
|
|
|
533
540
|
manifest_content = Path(result["manifest_file"]).read_text()
|
|
@@ -537,10 +544,10 @@ class TestManifestPathSeparators:
|
|
|
537
544
|
assert "\\" not in line, f"Backslash found in manifest path: {line}"
|
|
538
545
|
|
|
539
546
|
|
|
540
|
-
class
|
|
541
|
-
"""
|
|
547
|
+
class TestManifestAlwaysGenerated:
|
|
548
|
+
"""Manifest is always generated regardless of layout."""
|
|
542
549
|
|
|
543
|
-
def
|
|
550
|
+
def test_nested_always_generates_manifest(self, tmp_path):
|
|
544
551
|
root = _make_page("1", "Root")
|
|
545
552
|
child = _make_page("2", "Child")
|
|
546
553
|
pages = {"1": root, "2": child}
|
|
@@ -553,17 +560,16 @@ class TestNestedLayoutNoAutoManifest:
|
|
|
553
560
|
output_dir=str(tmp_path),
|
|
554
561
|
recursive=True,
|
|
555
562
|
layout="nested",
|
|
556
|
-
generate_manifest=False,
|
|
557
563
|
)
|
|
558
564
|
|
|
559
|
-
assert result["summary"]["manifest_generated"] is
|
|
560
|
-
assert result["manifest_file"] is None
|
|
561
|
-
assert
|
|
565
|
+
assert result["summary"]["manifest_generated"] is True
|
|
566
|
+
assert result["manifest_file"] is not None
|
|
567
|
+
assert (tmp_path / "confpub.yaml").exists()
|
|
562
568
|
|
|
563
569
|
|
|
564
570
|
class TestManifestFlag:
|
|
565
|
-
def
|
|
566
|
-
"""
|
|
571
|
+
def test_single_page_generates_manifest(self, tmp_path):
|
|
572
|
+
"""Manifest is always generated, even for a single page."""
|
|
567
573
|
page = _make_page("1", "Solo Page")
|
|
568
574
|
client = _mock_client({"1": page})
|
|
569
575
|
|
|
@@ -571,7 +577,6 @@ class TestManifestFlag:
|
|
|
571
577
|
result = pull_pages(
|
|
572
578
|
page_id="1",
|
|
573
579
|
output_dir=str(tmp_path),
|
|
574
|
-
generate_manifest=True,
|
|
575
580
|
)
|
|
576
581
|
|
|
577
582
|
assert result["summary"]["manifest_generated"] is True
|
|
@@ -580,3 +585,89 @@ class TestManifestFlag:
|
|
|
580
585
|
assert manifest.exists()
|
|
581
586
|
content = manifest.read_text()
|
|
582
587
|
assert "Solo Page" in content
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# ---------------------------------------------------------------------------
|
|
591
|
+
# Front matter tests
|
|
592
|
+
# ---------------------------------------------------------------------------
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
class TestBuildFrontMatter:
|
|
596
|
+
def test_basic_fields(self):
|
|
597
|
+
fm = _build_front_matter("My Page", "123", "SD")
|
|
598
|
+
assert fm.startswith("---\n")
|
|
599
|
+
assert fm.endswith("---\n\n")
|
|
600
|
+
parsed = yaml.safe_load(fm.strip("- \n"))
|
|
601
|
+
assert parsed["title"] == "My Page"
|
|
602
|
+
assert parsed["page_id"] == "123"
|
|
603
|
+
assert parsed["space"] == "SD"
|
|
604
|
+
assert "parent" not in parsed
|
|
605
|
+
assert "labels" not in parsed
|
|
606
|
+
|
|
607
|
+
def test_with_parent_and_labels(self):
|
|
608
|
+
fm = _build_front_matter("Child", "456", "SD", parent="Parent Page", labels=["a", "b"])
|
|
609
|
+
parsed = yaml.safe_load(fm.strip("- \n"))
|
|
610
|
+
assert parsed["parent"] == "Parent Page"
|
|
611
|
+
assert parsed["labels"] == ["a", "b"]
|
|
612
|
+
|
|
613
|
+
def test_empty_labels_omitted(self):
|
|
614
|
+
fm = _build_front_matter("Page", "1", "SD", labels=[])
|
|
615
|
+
parsed = yaml.safe_load(fm.strip("- \n"))
|
|
616
|
+
assert "labels" not in parsed
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class TestFrontMatterInPulledFiles:
|
|
620
|
+
def test_front_matter_fields_correct(self, tmp_path):
|
|
621
|
+
"""Pulled files contain correct front matter metadata."""
|
|
622
|
+
root = _make_page("1", "Root", space_key="SD")
|
|
623
|
+
child = _make_page("2", "Child Page", "<p>Body</p>", space_key="SD")
|
|
624
|
+
pages = {"1": root, "2": child}
|
|
625
|
+
children = {"1": [child]}
|
|
626
|
+
client = _mock_client(pages, children)
|
|
627
|
+
client.get_labels = lambda pid: [{"name": "my-label"}] if pid == "2" else []
|
|
628
|
+
|
|
629
|
+
with patch("confpub.puller.build_client", return_value=client):
|
|
630
|
+
pull_pages(
|
|
631
|
+
page_id="1",
|
|
632
|
+
output_dir=str(tmp_path),
|
|
633
|
+
recursive=True,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
# Check child file front matter
|
|
637
|
+
child_content = (tmp_path / "child-page.md").read_text()
|
|
638
|
+
assert child_content.startswith("---\n")
|
|
639
|
+
# Extract YAML block
|
|
640
|
+
parts = child_content.split("---\n", 2)
|
|
641
|
+
fm = yaml.safe_load(parts[1])
|
|
642
|
+
assert fm["title"] == "Child Page"
|
|
643
|
+
assert fm["page_id"] == "2"
|
|
644
|
+
assert fm["space"] == "SD"
|
|
645
|
+
assert fm["parent"] == "Root"
|
|
646
|
+
assert fm["labels"] == ["my-label"]
|
|
647
|
+
|
|
648
|
+
def test_root_page_has_no_parent_when_no_ancestors(self, tmp_path):
|
|
649
|
+
"""Root page omits parent when Confluence has no ancestors."""
|
|
650
|
+
page = _make_page("1", "Root")
|
|
651
|
+
client = _mock_client({"1": page})
|
|
652
|
+
|
|
653
|
+
with patch("confpub.puller.build_client", return_value=client):
|
|
654
|
+
pull_pages(page_id="1", output_dir=str(tmp_path))
|
|
655
|
+
|
|
656
|
+
content = (tmp_path / "root.md").read_text()
|
|
657
|
+
parts = content.split("---\n", 2)
|
|
658
|
+
fm = yaml.safe_load(parts[1])
|
|
659
|
+
assert "parent" not in fm
|
|
660
|
+
|
|
661
|
+
def test_root_page_has_parent_from_ancestors(self, tmp_path):
|
|
662
|
+
"""Root page gets parent from Confluence ancestors."""
|
|
663
|
+
page = _make_page("1", "Root")
|
|
664
|
+
client = _mock_client({"1": page})
|
|
665
|
+
client.get_page_ancestors = lambda pid: [{"title": "Space Home"}]
|
|
666
|
+
|
|
667
|
+
with patch("confpub.puller.build_client", return_value=client):
|
|
668
|
+
pull_pages(page_id="1", output_dir=str(tmp_path))
|
|
669
|
+
|
|
670
|
+
content = (tmp_path / "root.md").read_text()
|
|
671
|
+
parts = content.split("---\n", 2)
|
|
672
|
+
fm = yaml.safe_load(parts[1])
|
|
673
|
+
assert fm["parent"] == "Space Home"
|
|
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
|