confpub-cli 1.7.2__tar.gz → 1.7.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.
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/PKG-INFO +1 -1
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/__init__.py +1 -1
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/cli.py +0 -2
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/puller.py +77 -25
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_puller.py +106 -15
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/.github/copilot-instructions.md +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/.github/workflows/publish.yml +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/.gitignore +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/CLAUDE.md +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/LICENSE +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/PRD.md +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/README.md +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/applier.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/assets.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/config.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/confluence.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/converter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/envelope.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/errors.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/front_matter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/guide.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/lockfile.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/macro_plugin.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/manifest.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/output.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/planner.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/publish.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/py.typed +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/reverse_converter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/validator.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub/verifier.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/confpub.lock +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/pyproject.toml +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/__init__.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/conftest.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_applier.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_assets.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_config.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_confluence.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_converter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_envelope.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_errors.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_front_matter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_guide.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_integration.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_lockfile.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_macro_plugin.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_manifest.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_output.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_planner.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_publish.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_reverse_converter.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_validator.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/tests/test_verifier.py +0 -0
- {confpub_cli-1.7.2 → confpub_cli-1.7.3}/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.3
|
|
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
|
|
@@ -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)
|
|
@@ -224,6 +230,27 @@ def _build_page_tree(
|
|
|
224
230
|
return [root_entry]
|
|
225
231
|
|
|
226
232
|
|
|
233
|
+
def _build_front_matter(
|
|
234
|
+
title: str,
|
|
235
|
+
page_id: str,
|
|
236
|
+
space: str,
|
|
237
|
+
parent: str | None = None,
|
|
238
|
+
labels: list[str] | None = None,
|
|
239
|
+
) -> str:
|
|
240
|
+
"""Build a YAML front matter block for a pulled markdown file."""
|
|
241
|
+
data: dict[str, Any] = {
|
|
242
|
+
"title": title,
|
|
243
|
+
"page_id": page_id,
|
|
244
|
+
"space": space,
|
|
245
|
+
}
|
|
246
|
+
if parent:
|
|
247
|
+
data["parent"] = parent
|
|
248
|
+
if labels:
|
|
249
|
+
data["labels"] = labels
|
|
250
|
+
yaml_str = yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
251
|
+
return f"---\n{yaml_str}---\n\n"
|
|
252
|
+
|
|
253
|
+
|
|
227
254
|
def pull_pages(
|
|
228
255
|
*,
|
|
229
256
|
space: str | None = None,
|
|
@@ -234,7 +261,6 @@ def pull_pages(
|
|
|
234
261
|
force: bool = False,
|
|
235
262
|
layout: str = "flat",
|
|
236
263
|
include_attachments: bool = True,
|
|
237
|
-
generate_manifest: bool = False,
|
|
238
264
|
) -> dict[str, Any]:
|
|
239
265
|
"""Pull pages from Confluence to local Markdown files.
|
|
240
266
|
|
|
@@ -272,6 +298,19 @@ def pull_pages(
|
|
|
272
298
|
# Check for conflicts
|
|
273
299
|
_check_conflicts(file_paths, force)
|
|
274
300
|
|
|
301
|
+
# Build lookup maps for front matter parent resolution
|
|
302
|
+
id_to_title: dict[str, str] = {}
|
|
303
|
+
id_to_parent: dict[str, str | None] = {}
|
|
304
|
+
for entry in all_pages:
|
|
305
|
+
page = entry["page"]
|
|
306
|
+
pid = str(page["id"])
|
|
307
|
+
id_to_title[pid] = page.get("title", "")
|
|
308
|
+
id_to_parent[pid] = str(entry["parent_id"]) if entry["parent_id"] else None
|
|
309
|
+
|
|
310
|
+
# Get the root page's parent in Confluence (for front matter + manifest)
|
|
311
|
+
ancestors = client.get_page_ancestors(root_id)
|
|
312
|
+
root_parent_title = ancestors[-1].get("title", "") if ancestors else None
|
|
313
|
+
|
|
275
314
|
# Process each page
|
|
276
315
|
files_result: list[dict[str, Any]] = []
|
|
277
316
|
total_attachments = 0
|
|
@@ -288,27 +327,45 @@ def pull_pages(
|
|
|
288
327
|
# Download attachments
|
|
289
328
|
attachment_map: dict[str, str] = {}
|
|
290
329
|
attachments_downloaded = 0
|
|
330
|
+
out_path = file_paths[pid]
|
|
291
331
|
if include_attachments:
|
|
292
332
|
attachment_map = _download_page_attachments(
|
|
293
333
|
client, pid, slug, output_dir, layout, pull_warnings,
|
|
334
|
+
file_path=out_path,
|
|
294
335
|
)
|
|
295
336
|
attachments_downloaded = len(attachment_map)
|
|
296
337
|
total_attachments += attachments_downloaded
|
|
297
338
|
|
|
298
339
|
# Convert storage format to markdown
|
|
299
340
|
body_storage = page.get("body", {}).get("storage", {}).get("value", "")
|
|
300
|
-
|
|
341
|
+
conv_result = convert_storage_to_markdown(
|
|
301
342
|
body_storage, attachment_map=attachment_map,
|
|
302
343
|
)
|
|
303
344
|
|
|
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
345
|
# Fetch labels
|
|
310
346
|
page_labels = [lbl["name"] for lbl in client.get_labels(pid)]
|
|
311
347
|
|
|
348
|
+
# Determine parent title for front matter
|
|
349
|
+
if pid == root_id:
|
|
350
|
+
parent_title = root_parent_title
|
|
351
|
+
else:
|
|
352
|
+
par_id = id_to_parent.get(pid)
|
|
353
|
+
parent_title = id_to_title.get(par_id) if par_id else None
|
|
354
|
+
|
|
355
|
+
# Build and prepend front matter
|
|
356
|
+
front_matter = _build_front_matter(
|
|
357
|
+
title=page_title,
|
|
358
|
+
page_id=pid,
|
|
359
|
+
space=root_space,
|
|
360
|
+
parent=parent_title,
|
|
361
|
+
labels=page_labels,
|
|
362
|
+
)
|
|
363
|
+
markdown_content = front_matter + conv_result.markdown
|
|
364
|
+
|
|
365
|
+
# Write markdown file
|
|
366
|
+
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
|
367
|
+
Path(out_path).write_text(markdown_content, encoding="utf-8")
|
|
368
|
+
|
|
312
369
|
files_result.append({
|
|
313
370
|
"page_id": pid,
|
|
314
371
|
"title": page_title,
|
|
@@ -318,22 +375,17 @@ def pull_pages(
|
|
|
318
375
|
"labels": page_labels,
|
|
319
376
|
})
|
|
320
377
|
|
|
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
|
|
378
|
+
# Always generate manifest
|
|
379
|
+
root_title = root_page.get("title", "")
|
|
380
|
+
manifest_parent = ancestors[-1].get("title", root_title) if ancestors else root_title
|
|
381
|
+
pulled_labels: dict[str, list[str]] = {
|
|
382
|
+
f["page_id"]: f.get("labels", []) for f in files_result
|
|
383
|
+
}
|
|
384
|
+
page_tree = _build_page_tree(all_pages, file_paths, root_id, output_dir, page_labels=pulled_labels)
|
|
385
|
+
manifest_yaml = generate_manifest_yaml(root_space, manifest_parent, page_tree)
|
|
386
|
+
manifest_path = os.path.join(output_dir, "confpub.yaml")
|
|
387
|
+
Path(manifest_path).write_text(manifest_yaml, encoding="utf-8")
|
|
388
|
+
manifest_file: str = manifest_path
|
|
337
389
|
|
|
338
390
|
# Update lockfile
|
|
339
391
|
lockfile_path = os.path.join(output_dir, "confpub.lock")
|
|
@@ -356,6 +408,6 @@ def pull_pages(
|
|
|
356
408
|
"summary": {
|
|
357
409
|
"pages_pulled": len(files_result),
|
|
358
410
|
"attachments_downloaded": total_attachments,
|
|
359
|
-
"manifest_generated":
|
|
411
|
+
"manifest_generated": True,
|
|
360
412
|
},
|
|
361
413
|
}
|
|
@@ -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
|
|
File without changes
|